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-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

// ❌ 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

  1. External libraries
  2. Internal modules (absolute imports)
  3. Relative imports
  4. Types
  5. 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 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:

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

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