18 KiB
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
{
"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-plugineslint-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
{
"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
{
"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
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
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
// ❌ 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
// ❌ Bad
const foo = 1; // Never used
// ✅ Good
const _foo = 1; // Prefix with _ to ignore
@typescript-eslint/no-floating-promises - Requires await/catch
// ❌ Bad
asyncFunction(); // Promise not handled
// ✅ Good
await asyncFunction();
asyncFunction().catch(console.error);
void asyncFunction(); // Explicitly ignore
react-hooks/exhaustive-deps - Validates useEffect dependencies
// ❌ Bad
useEffect(() => {
fetchUser(userId);
}, []); // Missing userId dependency
// ✅ Good
useEffect(() => {
fetchUser(userId);
}, [userId]);
Prettier Configuration
.prettierrc
{
"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
# 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
const userName = 'John';
const isActive = true;
const totalCount = 100;
Constants: UPPER_SNAKE_CASE
const API_URL = 'http://localhost:4000';
const MAX_RETRIES = 3;
const DEFAULT_PAGE_SIZE = 50;
Functions: camelCase
function getUserById(id: number) {}
async function fetchCampaigns() {}
const handleClick = () => {};
Private methods: Prefix with underscore (optional)
class UserService {
async getUser(id: number) {}
private async _hashPassword(password: string) {}
}
Types and Interfaces
Types/Interfaces: PascalCase
interface User {
id: number;
email: string;
}
type UserRole = 'USER' | 'ADMIN';
interface CreateUserInput {
email: string;
password: string;
}
Enums: PascalCase, members UPPER_SNAKE_CASE
enum UserRole {
USER = 'USER',
ADMIN = 'ADMIN',
SUPER_ADMIN = 'SUPER_ADMIN'
}
React Components
Components: PascalCase
export function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>;
}
Props interfaces: ComponentNameProps
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
}
export function UserCard({ user, onEdit }: UserCardProps) {
return <div>{user.name}</div>;
}
Event handlers: handle[Event] or on[Event]
function UserForm() {
const handleSubmit = () => {};
const onEmailChange = (email: string) => {};
return <form onSubmit={handleSubmit}>...</form>;
}
Database Models
Prisma models: PascalCase (singular)
model User {
id Int @id @default(autoincrement())
email String @unique
}
model Campaign {
id Int @id @default(autoincrement())
title String
}
Table names: snake_case (plural)
model User {
@@map("users")
}
model Campaign {
@@map("campaigns")
}
Fields: camelCase in schema, snake_case in database
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
- External libraries
- Internal modules (absolute imports)
- Relative imports
- Types
- Styles (frontend)
// 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)
// auth.service.ts
export class AuthService {
async login() {}
}
// usage
import { AuthService } from './auth.service';
Default exports (React components)
// UserCard.tsx
export default function UserCard() {
return <div>...</div>;
}
// usage
import UserCard from './UserCard';
Re-exports (index files)
// 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:
async function getUser(id: number) {
const user = await prisma.user.findUnique({ where: { id } });
return user;
}
Bad:
function getUser(id: number) {
return prisma.user.findUnique({ where: { id } }).then(user => {
return user;
});
}
Error Handling
Use try/catch for error handling:
Good:
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:
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:
const email = user?.email;
const city = user?.address?.city;
Bad:
const email = user && user.email;
const city = user && user.address && user.address.city;
Nullish Coalescing
Use ?? for default values (not ||):
Good:
const limit = query.limit ?? 50;
const name = user.name ?? 'Unknown';
Bad:
const limit = query.limit || 50; // Fails for 0
const name = user.name || 'Unknown'; // Fails for ''
Array Methods
Prefer functional array methods:
Good:
const activeUsers = users.filter(u => u.isActive);
const emails = users.map(u => u.email);
const total = amounts.reduce((sum, amt) => sum + amt, 0);
Bad:
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:
const { email, name, role } = user;
const { limit = 50, page = 1 } = query;
Bad:
const email = user.email;
const name = user.name;
const role = user.role;
Template Literals
Use template literals for string interpolation:
Good:
const message = `Hello, ${user.name}!`;
const url = `/api/users/${userId}`;
Bad:
const message = 'Hello, ' + user.name + '!';
const url = '/api/users/' + userId;
Comments and Documentation
JSDoc for Functions
Document public functions with JSDoc:
/**
* 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<User> {
// ...
}
Inline Comments
Use inline comments for complex logic:
// 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:
const isValid = email.includes('@');
Bad:
// Check if email is valid
const isValid = email.includes('@');
TODO Comments
Use TODO for future work:
// 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:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat:New featurefix:Bug fixdocs:Documentationstyle:Formattingrefactor:Code restructuringtest:Adding testschore:Maintenance
Examples:
feat(auth): add JWT refresh token rotation
fix(map): correct point-in-polygon calculation
docs(api): update authentication guide
refactor(users): extract service layer
test(campaigns): add unit tests for CRUD operations
With scope and body:
git commit -m "feat(campaigns): add email sending
Implements BullMQ queue for async email delivery.
Adds retry logic and error handling.
Closes #123"
Co-Authoring with Claude
When Claude assists with code:
git commit -m "$(cat <<'EOF'
feat(auth): add JWT refresh token rotation
Implemented atomic refresh token rotation to prevent
race conditions during concurrent refresh requests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
React Patterns
Functional Components
Always use functional components (not class components):
Good:
export function UserCard({ user }: UserCardProps) {
return <div>{user.name}</div>;
}
Bad:
export class UserCard extends React.Component<UserCardProps> {
render() {
return <div>{this.props.user.name}</div>;
}
}
Hooks
Use hooks for state and side effects:
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchUsers() {
setLoading(true);
const data = await api.get('/users');
setUsers(data);
setLoading(false);
}
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
return <div>{users.map(u => <UserCard key={u.id} user={u} />)}</div>;
}
Props Destructuring
Destructure props in function signature:
Good:
function UserCard({ user, onEdit }: UserCardProps) {
return <div onClick={() => onEdit?.(user)}>{user.name}</div>;
}
Bad:
function UserCard(props: UserCardProps) {
return <div onClick={() => props.onEdit?.(props.user)}>{props.user.name}</div>;
}
Key Prop
Always provide key for list items:
Good:
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
Bad:
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}
Editor Integration
VSCode Settings
Create .vscode/settings.json:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
Pre-commit Hook
Install husky for pre-commit checks:
npm install --save-dev husky lint-staged
npx husky install
package.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
"scripts": {
"prepare": "husky install"
}
}
.husky/pre-commit:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
Quick Reference
Run Linting
# Lint
npm run lint
# Auto-fix
npm run lint:fix
# Format
npm run format
# Type-check
npm run type-check
Common Fixes
# Fix all auto-fixable issues
npm run lint:fix && npm run format
# Type-check both projects
cd api && npm run type-check && cd ../admin && npm run type-check
Related Documentation
- Setup: Local Development Setup
- TypeScript: TypeScript Guide
- Testing: Testing Guide
- Git: Git Workflow
Summary
You now know:
- ✅ TypeScript configuration (strict mode)
- ✅ ESLint rules and plugins
- ✅ Prettier configuration
- ✅ Naming conventions (files, variables, types)
- ✅ File organization patterns
- ✅ Code patterns (async/await, error handling)
- ✅ Comment and documentation standards
- ✅ Git commit message format
- ✅ React patterns and best practices
- ✅ Editor integration (VSCode, pre-commit hooks)
Quick Start:
# Auto-fix and format
npm run lint:fix && npm run format
# Check types
npm run type-check
# Pre-commit
npm run lint:fix && npm run format && npm run type-check