# Code Style Guide Coding standards and style conventions for Changemaker Lite V2. ## Overview Consistent code style improves: - **Readability:** Easier to understand code - **Maintainability:** Easier to modify code - **Collaboration:** Reduces merge conflicts - **Quality:** Catches common errors This guide covers TypeScript, ESLint, Prettier, and naming conventions. ## Tools ### TypeScript **Version:** 5.x **Config:** `tsconfig.json` (api/ and admin/) **Strict Mode:** Enabled ```json { "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true } } ``` ### ESLint **Version:** 8.x **Config:** `.eslintrc.js` (api/ and admin/) **Plugins:** - `@typescript-eslint/eslint-plugin` - `eslint-plugin-react` (admin only) - `eslint-plugin-react-hooks` (admin only) ### Prettier **Version:** 3.x **Config:** `.prettierrc` **Format on save:** Enabled (VSCode) ## TypeScript Configuration ### API tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "node", "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts"] } ``` ### Admin tsconfig.json ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "types": ["vite/client"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } ``` ## ESLint Rules ### API .eslintrc.js ```javascript module.exports = { parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 2022, sourceType: 'module', project: './tsconfig.json' }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking' ], plugins: ['@typescript-eslint'], root: true, env: { node: true, es2022: true }, rules: { // TypeScript '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_' } ], '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/await-thenable': 'error', // General 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-debugger': 'error', 'prefer-const': 'error', 'no-var': 'error', 'eqeqeq': ['error', 'always'], 'curly': ['error', 'all'] } }; ``` ### Admin .eslintrc.js ```javascript module.exports = { parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true }, project: './tsconfig.json' }, extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:@typescript-eslint/recommended' ], plugins: ['react', 'react-hooks', '@typescript-eslint'], root: true, env: { browser: true, es2020: true }, settings: { react: { version: 'detect' } }, rules: { // React 'react/react-in-jsx-scope': 'off', // React 17+ 'react/prop-types': 'off', // Use TypeScript 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', // TypeScript '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_' } ], // General 'no-console': ['warn', { allow: ['warn', 'error'] }], 'no-debugger': 'error', 'prefer-const': 'error', 'no-var': 'error', 'eqeqeq': ['error', 'always'] } }; ``` ### Key Rules Explained **`@typescript-eslint/no-explicit-any`** - Prevents `any` type ```typescript // ❌ Bad function foo(data: any) {} // ✅ Good function foo(data: User) {} function foo(data: unknown) {} // Use unknown instead ``` **`@typescript-eslint/no-unused-vars`** - Prevents unused variables ```typescript // ❌ Bad const foo = 1; // Never used // ✅ Good const _foo = 1; // Prefix with _ to ignore ``` **`@typescript-eslint/no-floating-promises`** - Requires await/catch ```typescript // ❌ Bad asyncFunction(); // Promise not handled // ✅ Good await asyncFunction(); asyncFunction().catch(console.error); void asyncFunction(); // Explicitly ignore ``` **`react-hooks/exhaustive-deps`** - Validates useEffect dependencies ```typescript // ❌ Bad useEffect(() => { fetchUser(userId); }, []); // Missing userId dependency // ✅ Good useEffect(() => { fetchUser(userId); }, [userId]); ``` ## Prettier Configuration ### .prettierrc ```json { "semi": true, "singleQuote": true, "tabWidth": 2, "useTabs": false, "trailingComma": "es5", "printWidth": 100, "arrowParens": "avoid", "endOfLine": "lf" } ``` ### .prettierignore ``` node_modules dist build coverage .vite .cache *.min.js *.min.css package-lock.json ``` ### Format Commands ```bash # Format all files npm run format # Check formatting (CI) npm run format:check # Format specific file npx prettier --write src/modules/auth/auth.service.ts ``` ## Naming Conventions ### Files and Directories **Files:** kebab-case ``` auth.service.ts user.controller.ts campaign.routes.ts locations-page.tsx ``` **Components:** PascalCase ``` UserCard.tsx LoginForm.tsx MapView.tsx ``` **Test files:** Match source file with `.test` or `.spec` ``` auth.service.test.ts UserCard.test.tsx ``` **Directories:** kebab-case ``` src/modules/auth/ src/components/map/ src/pages/public/ ``` ### Variables and Functions **Variables:** camelCase ```typescript const userName = 'John'; const isActive = true; const totalCount = 100; ``` **Constants:** UPPER_SNAKE_CASE ```typescript const API_URL = 'http://localhost:4000'; const MAX_RETRIES = 3; const DEFAULT_PAGE_SIZE = 50; ``` **Functions:** camelCase ```typescript function getUserById(id: number) {} async function fetchCampaigns() {} const handleClick = () => {}; ``` **Private methods:** Prefix with underscore (optional) ```typescript class UserService { async getUser(id: number) {} private async _hashPassword(password: string) {} } ``` ### Types and Interfaces **Types/Interfaces:** PascalCase ```typescript interface User { id: number; email: string; } type UserRole = 'USER' | 'ADMIN'; interface CreateUserInput { email: string; password: string; } ``` **Enums:** PascalCase, members UPPER_SNAKE_CASE ```typescript enum UserRole { USER = 'USER', ADMIN = 'ADMIN', SUPER_ADMIN = 'SUPER_ADMIN' } ``` ### React Components **Components:** PascalCase ```typescript export function UserCard({ user }: { user: User }) { return
{user.name}
; } ``` **Props interfaces:** ComponentNameProps ```typescript interface UserCardProps { user: User; onEdit?: (user: User) => void; } export function UserCard({ user, onEdit }: UserCardProps) { return
{user.name}
; } ``` **Event handlers:** handle[Event] or on[Event] ```typescript function UserForm() { const handleSubmit = () => {}; const onEmailChange = (email: string) => {}; return
...
; } ``` ### Database Models **Prisma models:** PascalCase (singular) ```prisma model User { id Int @id @default(autoincrement()) email String @unique } model Campaign { id Int @id @default(autoincrement()) title String } ``` **Table names:** snake_case (plural) ```prisma model User { @@map("users") } model Campaign { @@map("campaigns") } ``` **Fields:** camelCase in schema, snake_case in database ```prisma model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") } ``` ## File Organization ### Module Structure ``` src/modules/auth/ ├── auth.service.ts # Business logic ├── auth.routes.ts # Express routes ├── auth.schemas.ts # Zod validation schemas └── auth.service.test.ts # Tests ``` ### Import Order 1. External libraries 2. Internal modules (absolute imports) 3. Relative imports 4. Types 5. Styles (frontend) ```typescript // 1. External libraries import express from 'express'; import { z } from 'zod'; // 2. Internal modules import { authenticate } from '@/middleware/auth'; import { UserService } from '@/modules/users/user.service'; // 3. Relative imports import { AuthService } from './auth.service'; import { loginSchema } from './auth.schemas'; // 4. Types import type { Request, Response } from 'express'; import type { User } from '@prisma/client'; // 5. Styles (frontend only) import './auth.css'; ``` ### Export Patterns **Named exports** (preferred) ```typescript // auth.service.ts export class AuthService { async login() {} } // usage import { AuthService } from './auth.service'; ``` **Default exports** (React components) ```typescript // UserCard.tsx export default function UserCard() { return
...
; } // usage import UserCard from './UserCard'; ``` **Re-exports** (index files) ```typescript // modules/auth/index.ts export { AuthService } from './auth.service'; export { authRoutes } from './auth.routes'; export * from './auth.schemas'; ``` ## Code Patterns ### Async/Await Always use async/await (not callbacks or .then()): **Good:** ```typescript async function getUser(id: number) { const user = await prisma.user.findUnique({ where: { id } }); return user; } ``` **Bad:** ```typescript function getUser(id: number) { return prisma.user.findUnique({ where: { id } }).then(user => { return user; }); } ``` ### Error Handling Use try/catch for error handling: **Good:** ```typescript async function createUser(data: CreateUserInput) { try { const user = await prisma.user.create({ data }); return user; } catch (error) { logger.error('Failed to create user', error); throw new Error('User creation failed'); } } ``` **Bad:** ```typescript async function createUser(data: CreateUserInput) { const user = await prisma.user.create({ data }); // Unhandled error return user; } ``` ### Optional Chaining Use optional chaining for nullable values: **Good:** ```typescript const email = user?.email; const city = user?.address?.city; ``` **Bad:** ```typescript const email = user && user.email; const city = user && user.address && user.address.city; ``` ### Nullish Coalescing Use ?? for default values (not ||): **Good:** ```typescript const limit = query.limit ?? 50; const name = user.name ?? 'Unknown'; ``` **Bad:** ```typescript const limit = query.limit || 50; // Fails for 0 const name = user.name || 'Unknown'; // Fails for '' ``` ### Array Methods Prefer functional array methods: **Good:** ```typescript const activeUsers = users.filter(u => u.isActive); const emails = users.map(u => u.email); const total = amounts.reduce((sum, amt) => sum + amt, 0); ``` **Bad:** ```typescript const activeUsers = []; for (let i = 0; i < users.length; i++) { if (users[i].isActive) { activeUsers.push(users[i]); } } ``` ### Object Destructuring Use destructuring for object properties: **Good:** ```typescript const { email, name, role } = user; const { limit = 50, page = 1 } = query; ``` **Bad:** ```typescript const email = user.email; const name = user.name; const role = user.role; ``` ### Template Literals Use template literals for string interpolation: **Good:** ```typescript const message = `Hello, ${user.name}!`; const url = `/api/users/${userId}`; ``` **Bad:** ```typescript const message = 'Hello, ' + user.name + '!'; const url = '/api/users/' + userId; ``` ## Comments and Documentation ### JSDoc for Functions Document public functions with JSDoc: ```typescript /** * Creates a new user with the given email and password. * * @param email - User's email address * @param password - User's password (will be hashed) * @returns Created user object * @throws {Error} If user already exists */ async function createUser(email: string, password: string): Promise { // ... } ``` ### Inline Comments Use inline comments for complex logic: ```typescript // Calculate pagination offset const offset = (page - 1) * limit; // Hash password with 10 salt rounds const hashedPassword = await bcrypt.hash(password, 10); // Point-in-polygon ray-casting algorithm let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { // ... complex logic } ``` ### Avoid Obvious Comments Don't comment obvious code: **Good:** ```typescript const isValid = email.includes('@'); ``` **Bad:** ```typescript // Check if email is valid const isValid = email.includes('@'); ``` ### TODO Comments Use TODO for future work: ```typescript // TODO: Add pagination support async function getUsers() { return prisma.user.findMany(); } // FIXME: This doesn't handle edge case when user is null const userName = user.name; ``` ## Git Commit Messages ### Conventional Commits Use conventional commit format: ``` ():