# 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:
```
():