Standards & Reference
Code Quality
This document defines the code quality standards, practices, and tools for the Flowstate application.
Table of Contents
- Overview
- TypeScript Standards
- Functional Programming Principles
- Component Quality Standards
- Linting and Formatting
- Code Review Standards
- Performance Standards
- Maintainability Standards
- Quality Metrics
- Enforcement
Overview
Code quality is a fundamental pillar of the FlowState Standard. High-quality code reduces bugs, improves maintainability, and enhances developer productivity.
Quality Objectives
| Objective | Description |
|---|---|
| Correctness | Code behaves as specified |
| Reliability | Code handles edge cases gracefully |
| Maintainability | Code is easy to understand and modify |
| Performance | Code meets performance requirements |
| Security | Code follows security best practices |
| Testability | Code is designed for easy testing |
Quality Pillars
┌─────────────────────────────────────────────────────────────┐
│ Code Quality │
├─────────────┬─────────────┬─────────────┬──────────────────┤
│ Type Safe │ Functional │ Tested │ Documented │
│ TypeScript │ Principles │ Coverage │ Contracts │
└─────────────┴─────────────┴─────────────┴──────────────────┘
TypeScript Standards
Strict Type Checking
Enable all strict TypeScript options:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Type Definition Standards
// ✅ Good: Explicit, descriptive interfaces
interface EventDetails {
id: string;
name: string;
status: EventStatus;
startDate: Date;
endDate: Date;
deliverables: Deliverable[];
}
// ❌ Bad: Implicit any, loose typing
interface EventDetails {
id: any;
name: string;
status: string; // Should be union type
startDate: any; // Should be Date
deliverables: any[];
}
Union Types over Enums
Prefer union types for type safety with better tree-shaking:
// ✅ Preferred: Union types
type EventStatus = 'on-track' | 'at-risk' | 'delayed';
type DeliverableState = 'draft' | 'in-review' | 'approved' | 'rejected';
// Configuration objects for display
const EVENT_STATUS_CONFIG: Record<EventStatus, { label: string; color: string }> = {
'on-track': { label: 'On Track', color: 'green' },
'at-risk': { label: 'At Risk', color: 'yellow' },
'delayed': { label: 'Delayed', color: 'red' },
};
// ⚠️ Avoid: Enums (unless necessary for reverse mapping)
enum EventStatusEnum {
OnTrack = 'on-track',
AtRisk = 'at-risk',
Delayed = 'delayed',
}
Utility Types
Use TypeScript utility types effectively:
// Partial for optional updates
type EventUpdate = Partial<Event>;
// Pick for selecting properties
type EventSummary = Pick<Event, 'id' | 'name' | 'status'>;
// Omit for excluding properties
type NewEvent = Omit<Event, 'id' | 'createdAt' | 'updatedAt'>;
// Record for object types
type StatusColorMap = Record<EventStatus, string>;
// Required for making properties required
type RequiredEvent = Required<Event>;
// Readonly for immutable data
type ImmutableEvent = Readonly<Event>;
Generics
Use generics for reusable, type-safe code:
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
error: string | null;
status: number;
}
// Generic list component props
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
// Generic service pattern
class DataService<T extends { id: string }> {
async get(id: string): Promise<T | null> { /* ... */ }
async list(): Promise<T[]> { /* ... */ }
async create(data: Omit<T, 'id'>): Promise<T> { /* ... */ }
async update(id: string, data: Partial<T>): Promise<T> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
Discriminated Unions
Use discriminated unions for type-safe state handling:
// Result type for operations that can fail
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Async state with discriminated union
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// Usage with exhaustive checking
function handleAsyncState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return `Success: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error.message}`;
default:
// Exhaustive check - TypeScript error if a case is missing
const _exhaustive: never = state;
return _exhaustive;
}
}
Functional Programming Principles
Core Principles
These principles are foundational to code quality and directly enhance testability:
| Principle | Description | Benefit |
|---|---|---|
| Immutability | Data cannot be changed after creation | Eliminates side effects |
| Pure Functions | Same inputs always produce same outputs | Predictable, testable |
| Referential Transparency | Expressions can be replaced by values | Simplified debugging |
| First-Class Functions | Functions as values | Composition, modularity |
| Disciplined State | Controlled, explicit state changes | Easier reasoning |
Immutability
// ✅ Good: Immutable operations
const updateEvent = (event: Event, updates: Partial<Event>): Event => ({
...event,
...updates,
updatedAt: new Date(),
});
const addDeliverable = (event: Event, deliverable: Deliverable): Event => ({
...event,
deliverables: [...event.deliverables, deliverable],
});
// ❌ Bad: Mutating data
function updateEventBad(event: Event, updates: Partial<Event>): Event {
Object.assign(event, updates); // Mutation!
event.updatedAt = new Date(); // Mutation!
return event;
}
Pure Functions
// ✅ Good: Pure function
const calculateProgress = (deliverables: Deliverable[]): number => {
if (deliverables.length === 0) return 0;
const approved = deliverables.filter(d => d.status === 'approved').length;
return Math.round((approved / deliverables.length) * 100);
};
// ✅ Good: Pure function with date injection
const isOverdue = (dueDate: Date, currentDate: Date = new Date()): boolean => {
return dueDate < currentDate;
};
// ❌ Bad: Impure - depends on external state
let totalCalculations = 0;
function calculateProgressBad(deliverables: Deliverable[]): number {
totalCalculations++; // Side effect!
console.log('Calculating...'); // Side effect!
return /* calculation */;
}
Function Composition
// Compose utility
const compose = <T>(...fns: Array<(arg: T) => T>) =>
(initial: T): T => fns.reduceRight((acc, fn) => fn(acc), initial);
// Pipe utility (left-to-right composition)
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(initial: T): T => fns.reduce((acc, fn) => fn(acc), initial);
// Composable transformation functions
const sortByDate = (events: Event[]): Event[] =>
[...events].sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
const filterByStatus = (status: EventStatus) =>
(events: Event[]): Event[] => events.filter(e => e.status === status);
const limitTo = (count: number) =>
<T>(items: T[]): T[] => items.slice(0, count);
// Compose transformations
const getUpcomingDelayedEvents = pipe(
filterByStatus('delayed'),
sortByDate,
limitTo(5)
);
const result = getUpcomingDelayedEvents(allEvents);
Separation of Concerns
// ✅ Good: Separate pure logic from effects
// Pure business logic
const validateEvent = (event: Partial<Event>): ValidationResult => {
const errors: string[] = [];
if (!event.name?.trim()) errors.push('Name is required');
if (!event.startDate) errors.push('Start date is required');
if (event.endDate && event.startDate && event.endDate < event.startDate) {
errors.push('End date must be after start date');
}
return { valid: errors.length === 0, errors };
};
// Side effects at boundary
const saveEvent = async (event: Event): Promise<Result<Event>> => {
const validation = validateEvent(event);
if (!validation.valid) {
return { success: false, error: new ValidationError(validation.errors) };
}
try {
const saved = await api.events.save(event);
return { success: true, data: saved };
} catch (error) {
return { success: false, error: error as Error };
}
};
Component Quality Standards
Component Structure
// Standard component structure
import React, { useState, useCallback, useMemo } from 'react';
import { cn } from '@/lib/utils';
// 1. Type definitions at top
interface EventCardProps {
event: Event;
onSelect?: (event: Event) => void;
className?: string;
}
// 2. Constants and helper functions
const STATUS_STYLES = {
'on-track': 'bg-green-100 text-green-800',
'at-risk': 'bg-yellow-100 text-yellow-800',
'delayed': 'bg-red-100 text-red-800',
} as const;
// 3. Component definition
const EventCard: React.FC<EventCardProps> = ({
event,
onSelect,
className,
}) => {
// 4. Hooks at top of component
const [isHovered, setIsHovered] = useState(false);
// 5. Memoized values
const statusStyle = useMemo(
() => STATUS_STYLES[event.status],
[event.status]
);
// 6. Callbacks
const handleClick = useCallback(() => {
onSelect?.(event);
}, [event, onSelect]);
// 7. Render
return (
<div
className={cn('rounded-lg p-4', statusStyle, className)}
onClick={handleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Content */}
</div>
);
};
// 8. Export
export default EventCard;
export type { EventCardProps };
Component Composition
// Prefer composition over inheritance
// ✅ Good: Composable components
const Card: React.FC<CardProps> = ({ children, className }) => (
<div className={cn('rounded-lg border bg-white shadow-sm', className)}>
{children}
</div>
);
const CardHeader: React.FC<CardHeaderProps> = ({ children, className }) => (
<div className={cn('border-b p-4', className)}>{children}</div>
);
const CardContent: React.FC<CardContentProps> = ({ children, className }) => (
<div className={cn('p-4', className)}>{children}</div>
);
// Usage
const EventCard: React.FC<EventCardProps> = ({ event }) => (
<Card>
<CardHeader>
<h3>{event.name}</h3>
</CardHeader>
<CardContent>
<EventDetails event={event} />
</CardContent>
</Card>
);
Props Guidelines
// ✅ Good: Clear, typed props
interface ButtonProps {
/** Button content */
children: React.ReactNode;
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'danger';
/** Button size */
size?: 'sm' | 'md' | 'lg';
/** Disabled state */
disabled?: boolean;
/** Loading state - shows spinner */
loading?: boolean;
/** Click handler */
onClick?: () => void;
/** Additional CSS classes */
className?: string;
}
// ❌ Bad: Ambiguous, loosely typed props
interface ButtonProps {
children: any;
type?: string; // Too vague
size?: number; // Should be predefined sizes
style?: object; // Avoid inline styles
click?: Function; // Should be specific function type
}
Hook Quality Standards
// Custom hook structure
import { useState, useEffect, useCallback, useMemo } from 'react';
interface UseEventsOptions {
status?: EventStatus;
autoRefresh?: boolean;
refreshInterval?: number;
}
interface UseEventsReturn {
events: Event[];
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
filter: (status: EventStatus) => void;
}
/**
* Hook for managing event data with filtering and auto-refresh.
*
* @param options - Configuration options
* @returns Event data, loading state, and control functions
*
* @example
* ```tsx
* const { events, loading, refetch } = useEvents({
* status: 'on-track',
* autoRefresh: true,
* });
* ```
*/
export function useEvents(options: UseEventsOptions = {}): UseEventsReturn {
const { status, autoRefresh = false, refreshInterval = 30000 } = options;
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [filterStatus, setFilterStatus] = useState<EventStatus | undefined>(status);
const fetchEvents = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await eventService.getEvents({ status: filterStatus });
setEvents(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch events'));
} finally {
setLoading(false);
}
}, [filterStatus]);
useEffect(() => {
fetchEvents();
}, [fetchEvents]);
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(fetchEvents, refreshInterval);
return () => clearInterval(interval);
}, [autoRefresh, refreshInterval, fetchEvents]);
const filter = useCallback((newStatus: EventStatus) => {
setFilterStatus(newStatus);
}, []);
return { events, loading, error, refetch: fetchEvents, filter };
}
Linting and Formatting
ESLint Configuration
// .eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2022: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint', 'react', 'import'],
rules: {
// TypeScript
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
// React
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
'import/no-duplicates': 'error',
// General
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
eqeqeq: ['error', 'always'],
},
settings: {
react: { version: 'detect' },
'import/resolver': {
typescript: { project: './tsconfig.json' },
},
},
};
Prettier Configuration
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"jsxSingleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"]
}
Import Order
// Correct import order
// 1. React and core libraries
import React, { useState, useEffect } from 'react';
// 2. Third-party libraries
import { useQuery } from '@tanstack/react-query';
import { format } from 'date-fns';
import { z } from 'zod';
// 3. Internal aliases (@/)
import { Button } from '@/components/ui/button';
import { useAuth } from '@/hooks/useAuth';
import { eventService } from '@/services/eventService';
// 4. Relative imports
import { EventCard } from './EventCard';
import { statusConfig } from './config';
import type { EventCardProps } from './types';
Code Review Standards
Review Checklist
Functionality
- [ ] Code implements the requirements correctly
- [ ] Edge cases are handled
- [ ] Error handling is appropriate
- [ ] No regressions introduced
Code Quality
- [ ] TypeScript types are accurate and complete
- [ ] No
anytypes without justification - [ ] Functions are pure where possible
- [ ] Code is DRY (Don't Repeat Yourself)
- [ ] Complexity is appropriate
Testing
- [ ] Unit tests cover new functionality
- [ ] Edge cases have test coverage
- [ ] Tests are readable and maintainable
- [ ] No flaky tests introduced
Security
- [ ] No sensitive data exposed
- [ ] Input validation in place
- [ ] No security vulnerabilities
Performance
- [ ] No unnecessary re-renders
- [ ] Expensive operations are memoized
- [ ] No memory leaks
Accessibility
- [ ] Keyboard navigation works
- [ ] Screen reader compatible
- [ ] Color contrast sufficient
Review Comments
// Good review comment examples:
// 🐛 Bug: This will fail when deliverables array is empty
const progress = approved / deliverables.length * 100;
// 💡 Suggestion: Consider using useMemo to avoid recalculating on every render
const sortedEvents = events.sort((a, b) => a.date - b.date);
// ❓ Question: Should we handle the case where the user has no permissions?
if (user.canEdit) { /* ... */ }
// ⚠️ Warning: This creates a new function on every render, may cause
// unnecessary re-renders in child components
onClick={() => handleClick(item.id)}
// ✅ Praise: Great use of discriminated unions for type safety!
type Result<T> = { success: true; data: T } | { success: false; error: Error };
Performance Standards
React Performance
// ✅ Good: Memoized expensive calculations
const expensiveValue = useMemo(() => {
return events.reduce((acc, event) => {
return acc + calculateComplexMetric(event);
}, 0);
}, [events]);
// ✅ Good: Stable callback references
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []);
// ✅ Good: Memoized component
const EventCard = memo<EventCardProps>(({ event, onSelect }) => {
return (/* ... */);
});
// ❌ Bad: Object/array in dependency array recreated every render
useEffect(() => {
fetchData(filters);
}, [{ status: 'active' }]); // New object every render!
Bundle Size
| Category | Target | Action |
|---|---|---|
| Initial JS | < 200KB | Code splitting |
| Per-route | < 50KB | Lazy loading |
| Images | < 100KB each | Optimization |
| CSS | < 50KB | PurgeCSS |
// Lazy loading routes
const Dashboard = lazy(() => import('./views/Dashboard'));
const Reports = lazy(() => import('./views/Reports'));
const Settings = lazy(() => import('./views/Settings'));
// Lazy loading components
const HeavyChart = lazy(() => import('./components/HeavyChart'));
Performance Budgets
// package.json
{
"bundlesize": [
{
"path": "./dist/assets/index-*.js",
"maxSize": "200 kB"
},
{
"path": "./dist/assets/index-*.css",
"maxSize": "50 kB"
}
]
}
Maintainability Standards
File Organization
src/
├── components/ # Reusable UI components
│ ├── ui/ # Primitive components (Button, Card, etc.)
│ └── [Feature]/ # Feature-specific components
├── hooks/ # Custom React hooks
├── services/ # API and business logic
├── utils/ # Pure utility functions
├── types/ # Shared TypeScript types
├── lib/ # Third-party integrations
├── views/ # Page components
└── constants/ # Application constants
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | EventCard.tsx |
| Hooks | camelCase with use | useEvents.ts |
| Utilities | camelCase | formatDate.ts |
| Types | PascalCase | EventStatus |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Files | kebab-case | event-service.ts |
Documentation Requirements
/**
* Calculates the overall progress percentage for an event.
*
* @param event - The event to calculate progress for
* @returns Progress percentage from 0-100, or 0 if no deliverables
*
* @example
* ```ts
* const progress = calculateEventProgress(event);
* // Returns: 75
* ```
*
* @remarks
* Progress is calculated as: (approved / total) * 100
* Rounded to nearest integer.
*/
export function calculateEventProgress(event: Event): number {
const { deliverables } = event;
if (deliverables.length === 0) return 0;
const approved = deliverables.filter(d => d.status === 'approved').length;
return Math.round((approved / deliverables.length) * 100);
}
Quality Metrics
Code Quality Targets
| Metric | Target | Tool |
|---|---|---|
| Test Coverage | ≥ 80% | Vitest |
| Type Coverage | ≥ 95% | TypeScript |
| Cyclomatic Complexity | ≤ 10 | ESLint |
| Function Length | ≤ 50 lines | ESLint |
| File Length | ≤ 300 lines | ESLint |
| Lint Errors | 0 | ESLint |
| Security Issues | 0 critical/high | npm audit |
Automated Quality Checks
# .github/workflows/quality.yml
name: Quality Check
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Test with coverage
run: npm run test:coverage
- name: Check bundle size
run: npm run build && npm run analyze
Quality Dashboard
Track these metrics over time:
- Test coverage trends
- Bundle size trends
- TypeScript error counts
- ESLint warning counts
- Performance scores (Lighthouse)
- Accessibility scores
Enforcement
Pre-commit Hooks
# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
CI/CD Gates
| Gate | Requirement | Action on Failure |
|---|---|---|
| Lint | 0 errors | Block merge |
| TypeScript | 0 errors | Block merge |
| Tests | All passing | Block merge |
| Coverage | ≥ 80% | Block merge |
| Security | No critical | Block merge |
| Bundle Size | Within budget | Warning |
Quality Ownership
| Role | Responsibility |
|---|---|
| Developer | Write quality code, tests, documentation |
| Reviewer | Enforce standards during review |
| Tech Lead | Define and maintain standards |
| CI/CD | Automate enforcement |
Debugging Patterns
Learnings from development sessions on effective debugging approaches.
Authentication Flow Debugging
When debugging authentication issues, trace the full flow systematically:
1. JWT Claims → Check token has required claims (iss, sub, domainId)
2. Gateway Validation → Verify Kong validates and extracts claims
3. Header Injection → Confirm x-domain-id header is set correctly
4. Service Handling → Check downstream service reads and uses header
Component Debugging
// ✅ Good: Add diagnostic logging at component entry points first
export function MyComponent({ data }: Props) {
// Temporary debug logging - remove before commit
console.log('[MyComponent] Received data:', {
hasData: !!data,
dataLength: data?.length,
firstItem: data?.[0]
});
// ... component logic
}
// Then drill down into specific areas based on log output
API/Network Debugging
| Step | Action | Tool |
|---|---|---|
| 1 | Check request reaches server | Browser DevTools Network tab |
| 2 | Verify request headers | curl -v with same headers |
| 3 | Check server logs | docker logs <container> or ./scripts/local-env.sh logs |
| 4 | Verify response format | Compare expected vs actual response schema |
Common Gotchas
| Issue | Root Cause | Solution |
|---|---|---|
| 401 from Kong | Missing iss claim in JWT | Ensure auth server sets issuer config |
| Schema migration fails | Migration plugin not enabled | Add migration: true to RxDB plugins |
| Container has old code | Built from git clone without push | Push changes, then rebuild container |
| Embedding timeouts | Default 30s timeout too short | Use AbortSignal.timeout(120000) minimum |
Related Documents
- TDD.md - Test-driven development process
- BAT.md - Browser automation testing
- COMPLIANCE.md - Compliance requirements
- PROCESS.md - Development workflow