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-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¶
{
"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
@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¶
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
Components: PascalCase
Test files: Match source file with .test or .spec
Directories: kebab-case
Variables and Functions¶
Variables: camelCase
Constants: UPPER_SNAKE_CASE
Functions: camelCase
function getUserById(id: number) {}
async function fetchCampaigns() {}
const handleClick = () => {};
Private methods: Prefix with underscore (optional)
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
React Components¶
Components: PascalCase
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)
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:
Bad:
Nullish Coalescing¶
Use ?? for default values (not ||):
Good:
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:
Bad:
Template Literals¶
Use template literals for string interpolation:
Good:
Bad:
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:
Bad:
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:
Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation
- style: Formatting
- refactor: Code restructuring
- test: Adding tests
- chore: 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:
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:
Bad:
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:
package.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
},
"scripts": {
"prepare": "husky install"
}
}
.husky/pre-commit:
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: