Skip to content

Latest commit

 

History

History
436 lines (318 loc) · 10.4 KB

File metadata and controls

436 lines (318 loc) · 10.4 KB

Code Quality

Writing high-quality, maintainable code is essential for long-term project success. This guide covers the principles, practices, and tools that help ensure code quality in our monorepo.

Quality Principles

Simplicity First

KISS (Keep It Simple, Stupid)

  • Solutions should be as simple as possible while meeting requirements
  • Avoid unnecessary complexity and over-engineering
  • Prefer clear, straightforward approaches over clever but opaque solutions

YAGNI (You Aren't Gonna Need It)

  • Don't implement features or abstractions until they're actually required
  • Focus on solving current problems, not hypothetical future ones
  • Add complexity only when there's a proven need

Maintainability and Readability

Self-Documenting Code

  • Use meaningful names for variables, functions, and classes
  • Write code that explains its purpose without comments
  • Structure code logically and consistently

Modular Design

  • Break complex problems into smaller, manageable pieces
  • Each module should have a single, clear responsibility
  • Minimize coupling between modules

Testability

Design for Testing

  • Write code that's easy to test in isolation
  • Use dependency injection to enable mocking
  • Separate business logic from framework-specific code

Comprehensive Testing

  • Write tests for all critical functionality
  • Include edge cases and error conditions
  • Maintain high test coverage for important code paths

Quality Tools

Code Formatting

Prettier Integration

# Format all changed files
nx format:write

# Check formatting without making changes
nx format:check

Benefits:

  • Consistent code style across the entire codebase
  • Eliminates style debates and discussions
  • Reduces cognitive load when reading code
  • Prevents CI failures due to formatting issues

Linting

ESLint Configuration

# Lint all projects
nx run-many -t lint

# Lint specific project
nx run-many -t lint -p PROJECT_NAME

# Fix linting issues automatically
nx run-many -t lint --fix

What Linting Catches:

  • Potential bugs and logic errors
  • Code style violations
  • Security vulnerabilities
  • Performance issues
  • Unused code and imports

Testing

Comprehensive Test Suite

# Run all tests
nx run-many -t test

# Run tests for affected projects
nx affected -t test

# Run tests with coverage
nx run-many -t test --coverage

Test Types:

  • Unit Tests - Test individual components and functions
  • Integration Tests - Test how components work together
  • End-to-End Tests - Test complete user workflows
  • Visual Tests - Ensure UI components render correctly

Code Standards

Naming Conventions

Variables and Functions

// Good: Descriptive and clear
const userAccountBalance = calculateAccountBalance(userId);
const isValidEmailAddress = validateEmail(email);

// Bad: Unclear or abbreviated
const bal = calc(user);
const valid = check(email);

Classes and Interfaces

// Good: Clear purpose and responsibility
class UserAccountService {
  async createAccount(userData: CreateUserRequest): Promise<UserAccount> {
    // Implementation
  }
}

// Bad: Vague or overly generic
class Manager {
  async do(user: any): Promise<any> {
    // Implementation
  }
}

Files and Directories

// Good: Descriptive and consistent
user - account.service.ts;
user - account.component.ts;
user - account.interface.ts;

// Bad: Unclear or inconsistent
user.ts;
UserComponent.ts;
user_account_interface.ts;

Function Design

Single Responsibility

// Good: Each function has one clear purpose
function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function sendWelcomeEmail(email: string): Promise<void> {
  return emailService.send({
    to: email,
    subject: 'Welcome!',
    template: 'welcome',
  });
}

// Bad: Function does multiple things
function processUser(email: string): Promise<void> {
  // Validates email
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  if (!isValid) throw new Error('Invalid email');

  // Sends email
  return emailService.send({
    to: email,
    subject: 'Welcome!',
    template: 'welcome',
  });
}

Clear Parameters and Return Types

// Good: Explicit types and clear parameters
function calculateTotalPrice(items: CartItem[], taxRate: number, discountCode?: string): number {
  // Implementation
}

// Bad: Unclear types and parameters
function calc(items: any, tax: any, code?: any): any {
  // Implementation
}

Error Handling

Consistent Error Patterns

// Good: Specific error types and clear messages
class ValidationError extends Error {
  constructor(field: string, message: string) {
    super(`Validation failed for ${field}: ${message}`);
    this.name = 'ValidationError';
  }
}

function validateUser(user: User): void {
  if (!user.email) {
    throw new ValidationError('email', 'Email is required');
  }
  if (!isValidEmail(user.email)) {
    throw new ValidationError('email', 'Invalid email format');
  }
}

// Bad: Generic errors with unclear messages
function validateUser(user: any): void {
  if (!user.email) {
    throw new Error('Bad');
  }
}

Graceful Degradation

// Good: Handle errors gracefully
async function loadUserData(userId: string): Promise<UserData | null> {
  try {
    return await userService.getUser(userId);
  } catch (error) {
    console.error('Failed to load user data:', error);
    return null; // Graceful fallback
  }
}

// Bad: Let errors bubble up without handling
async function loadUserData(userId: string): Promise<UserData> {
  return await userService.getUser(userId); // Could throw
}

Testing Best Practices

Test Structure

Arrange-Act-Assert Pattern

describe('UserService', () => {
  describe('createUser', () => {
    it('should create a new user with valid data', async () => {
      // Arrange
      const userData = {
        email: 'test@example.com',
        name: 'Test User',
      };
      const mockUserRepository = {
        save: jest.fn().mockResolvedValue({ id: '1', ...userData }),
      };
      const userService = new UserService(mockUserRepository);

      // Act
      const result = await userService.createUser(userData);

      // Assert
      expect(result).toEqual({ id: '1', ...userData });
      expect(mockUserRepository.save).toHaveBeenCalledWith(userData);
    });
  });
});

Descriptive Test Names

// Good: Clear what is being tested
it('should return null when user is not found', async () => {
  // Test implementation
});

it('should throw ValidationError when email is invalid', async () => {
  // Test implementation
});

// Bad: Unclear what is being tested
it('should work', async () => {
  // Test implementation
});

it('test user', async () => {
  // Test implementation
});

Test Coverage

Focus on Critical Paths

  • Test all business logic and decision points
  • Include edge cases and error conditions
  • Test integration points between components
  • Don't test implementation details

Coverage Goals

  • Aim for high coverage of critical functionality
  • Don't sacrifice quality for coverage numbers
  • Focus on testing the right things, not everything

Code Review Guidelines

What to Look For

Functionality

  • Does the code do what it's supposed to do?
  • Are edge cases handled appropriately?
  • Are error conditions handled gracefully?

Code Quality

  • Is the code readable and maintainable?
  • Are naming conventions followed?
  • Is the code properly structured and organized?

Testing

  • Are there adequate tests for the changes?
  • Do the tests cover the right scenarios?
  • Are the tests clear and maintainable?

Architecture

  • Does the code follow established patterns?
  • Are dependencies appropriate and minimal?
  • Does it fit well with the existing codebase?

Review Process

Before Reviewing

  • Understand the context and requirements
  • Review the associated issue or PR description
  • Check if tests are included and passing

During Review

  • Focus on the most important aspects first
  • Provide constructive feedback with explanations
  • Suggest improvements, not just point out problems
  • Be respectful and professional

After Review

  • Follow up on feedback and questions
  • Help the author understand and implement suggestions
  • Approve when the code meets quality standards

Continuous Improvement

Regular Refactoring

When to Refactor

  • When code becomes difficult to understand or maintain
  • When you need to add features and the current structure doesn't support them
  • When you find better patterns or approaches
  • When technical debt accumulates

How to Refactor

  • Make small, incremental changes
  • Ensure good test coverage before refactoring
  • Refactor in dedicated commits, separate from feature changes
  • Validate that functionality remains the same

Learning and Growth

Stay Current

  • Keep up with best practices and new patterns
  • Learn from code reviews and team discussions
  • Experiment with new approaches in appropriate contexts
  • Share knowledge with the team

Measure and Improve

  • Use code quality metrics to identify areas for improvement
  • Regularly review and update coding standards
  • Collect feedback from the team on quality practices
  • Continuously refine processes and tools

Quality Checklist

Before submitting code, ask yourself:

  • Is the code simple and easy to understand?
  • Are variable and function names descriptive?
  • Does each function have a single responsibility?
  • Are error conditions handled appropriately?
  • Are there adequate tests for the functionality?
  • Does the code follow established patterns?
  • Is the code properly formatted and linted?
  • Are dependencies minimal and appropriate?
  • Does the code fit well with the existing codebase?
  • Is the code ready for others to maintain?

Next Steps

Now that you understand code quality principles:

  1. Explore the validation pipeline - Automated quality checks
  2. Review development workflows - Learn effective development processes
  3. Check out the tools - Understand the development tooling

Remember: Code quality is not just about making code work—it's about making code that works well for everyone who will encounter it in the future, including yourself.