Skip to content

Coding Standards for PRS

This document outlines the coding standards and best practices for the PRS (Purchase Requisition System), covering both backend and frontend development. These standards are based on the current codebase patterns and established practices.

General Guidelines

  1. Readability: Write code that is easy to read and understand
  2. Simplicity: Keep code simple and avoid unnecessary complexity
  3. Consistency: Follow consistent patterns and naming conventions
  4. Documentation: Document code appropriately with comments and JSDoc
  5. Testing: Write tests for all code

Naming Conventions

Variables and Functions

  • Use camelCase for variables and functions
  • Use descriptive names that clearly indicate purpose
  • Avoid abbreviations unless they are widely understood
  • Boolean variables should have prefixes like is, has, or should
JavaScript
1
2
3
4
5
6
7
// Good
const userProfile = getUserProfile(userId);
const isActive = user.status === 'ACTIVE';

// Bad
const up = getUP(uid);
const active = user.status === 'ACTIVE';

Classes

  • Use PascalCase for class names
  • Class names should be nouns
  • Use descriptive names that clearly indicate purpose
JavaScript
1
2
3
4
5
6
7
8
9
// Good
class UserService {
  // ...
}

// Bad
class Users {
  // ...
}

Constants

  • Use UPPER_SNAKE_CASE for constants
  • Group related constants in objects
JavaScript
// Good
const STATUS = {
  ACTIVE: 'ACTIVE',
  INACTIVE: 'INACTIVE',
  PENDING: 'PENDING',
};

// Bad
const ACTIVE = 'ACTIVE';
const INACTIVE = 'INACTIVE';
const PENDING = 'PENDING';

Files

  • Use kebab-case for file names
  • Use descriptive names that clearly indicate purpose
  • Group related files in directories
Text Only
1
2
3
4
5
6
7
8
9
src/
  app/
    services/
      user-service.js
      order-service.js
  domain/
    entities/
      user.js
      order.js

Code Structure

Functions

  • Keep functions small and focused on a single task
  • Limit function length to 30 lines where possible
  • Use early returns to reduce nesting
JavaScript
// Good
function validateUser(user) {
  if (!user) {
    return { valid: false, error: 'User is required' };
  }

  if (!user.email) {
    return { valid: false, error: 'Email is required' };
  }

  return { valid: true };
}

// Bad
function validateUser(user) {
  let result = { valid: false, error: null };

  if (user) {
    if (user.email) {
      result.valid = true;
    } else {
      result.error = 'Email is required';
    }
  } else {
    result.error = 'User is required';
  }

  return result;
}

Classes

  • Follow the Single Responsibility Principle
  • Keep classes focused on a specific domain concept
  • Use dependency injection for dependencies
  • Limit class methods to those that operate on the class's data
JavaScript
// Good
class UserService {
  constructor({ userRepository, emailService }) {
    this.userRepository = userRepository;
    this.emailService = emailService;
  }

  async createUser(userData) {
    const user = await this.userRepository.create(userData);
    await this.emailService.sendWelcomeEmail(user.email);
    return user;
  }
}

// Bad
class UserService {
  constructor() {
    this.db = require('../database');
    this.email = require('../email');
  }

  async createUser(userData) {
    const user = await this.db.users.create(userData);
    await this.email.sendWelcomeEmail(user.email);
    return user;
  }

  async getOrdersByUser(userId) {
    return this.db.orders.findAll({ where: { userId } });
  }
}

Error Handling

  • Use custom error classes for domain-specific errors
  • Always catch and handle errors appropriately
  • Log errors with context information
  • Return consistent error responses from API endpoints
JavaScript
// Good
try {
  const user = await userService.getById(id);
  return res.status(200).send(user);
} catch (error) {
  req.log.error({ error, userId: id }, 'Error getting user');

  if (error.name === 'NotFoundError') {
    return res.status(404).send({
      success: false,
      message: 'User not found',
    });
  }

  return res.status(500).send({
    success: false,
    message: 'Internal server error',
  });
}

// Bad
try {
  const user = await userService.getById(id);
  return res.status(200).send(user);
} catch (error) {
  console.error(error);
  return res.status(500).send('Error');
}

Comments

  • Use JSDoc for documenting functions and classes
  • Add comments for complex logic that is not immediately obvious
  • Avoid comments that simply restate the code
  • Keep comments up-to-date with code changes
JavaScript
/**
 * Creates a new user and sends a welcome email
 *
 * @param {Object} userData - User data
 * @param {string} userData.email - User email
 * @param {string} userData.name - User name
 * @returns {Promise<Object>} - Created user
 * @throws {ValidationError} - If user data is invalid
 */
async function createUser(userData) {
  // Validate user data
  const validationResult = validateUser(userData);

  if (!validationResult.valid) {
    throw new ValidationError(validationResult.error);
  }

  // Create user in database
  const user = await userRepository.create(userData);

  // Send welcome email
  await emailService.sendWelcomeEmail(user.email);

  return user;
}

PRS-Specific Patterns

Status Constants

Follow the established pattern for status constants:

JavaScript
// ✅ Correct - Use Object.freeze() for immutability
const CANVASS_STATUS = Object.freeze({
  DRAFT: 'draft',
  PARTIAL: 'partially_canvassed',
  FOR_APPROVAL: 'for_approval',
  REJECTED: 'rejected',
  APPROVED: 'approved',
});

// ❌ Incorrect - Missing Object.freeze() (legacy pattern)
const REQUISITION_STATUS = {
  DRAFT: 'draft',
  SUBMITTED: 'submitted',
  // ...
};

Controller Structure

Follow the established BaseController pattern:

JavaScript
// ✅ Correct - Extend BaseController
class RequisitionController extends BaseController {
  constructor({ requisitionService, requisitionRepository }) {
    super();
    this.requisitionService = requisitionService;
    this.requisitionRepository = requisitionRepository;
  }

  async createRequisition(request, reply) {
    const { body } = request;
    const { userFromToken } = request;
    const transaction = await this.sequelize.transaction();

    try {
      const requisition = await this.requisitionService.create(body, {
        transaction,
        userFromToken,
      });

      await transaction.commit();
      return this.success(reply, requisition);
    } catch (error) {
      await transaction.rollback();
      throw error;
    }
  }
}

Service Layer Pattern

Use dependency injection and transaction management:

JavaScript
// ✅ Correct - Service with dependency injection
class RequisitionService {
  constructor({ requisitionRepository, approverService, logger }) {
    this.requisitionRepository = requisitionRepository;
    this.approverService = approverService;
    this.logger = logger;
  }

  async create(data, { transaction, userFromToken }) {
    // Validate data
    const validationResult = this.validateRequisitionData(data);
    if (!validationResult.isValid) {
      throw new ValidationError(validationResult.errors);
    }

    // Create requisition
    const requisition = await this.requisitionRepository.create(data, {
      transaction,
    });

    // Assign approvers
    await this.approverService.assignApprovers({
      requisitionId: requisition.id,
      category: data.category,
      transaction,
    });

    return requisition;
  }
}

Error Handling Pattern

Use the established error handling pattern:

JavaScript
// ✅ Correct - Use error factory and structured logging
try {
  const result = await this.requisitionService.create(data);
  return this.success(reply, result);
} catch (error) {
  // Log with structured logging
  this.fastify.log.error(
    `Requisition creation failed - ${JSON.stringify(error)}`,
    {
      userId: userFromToken.id,
      data: sanitizeLogData(data),
      error: error.message,
    }
  );

  // Use error factory for consistent responses
  if (error.name === 'ValidationError') {
    throw this.clientErrors.BAD_REQUEST({
      message: error.message,
      details: error.details,
    });
  }

  throw this.serverErrors.INTERNAL_SERVER_ERROR({
    message: 'Failed to create requisition',
  });
}

Repository Pattern

Follow the established repository pattern:

JavaScript
// ✅ Correct - Repository with proper error handling
class RequisitionRepository extends BaseRepository {
  constructor({ models, logger }) {
    super();
    this.models = models;
    this.logger = logger;
  }

  async findByStatus(status, options = {}) {
    try {
      return await this.models.Requisition.findAll({
        where: { status },
        include: this.getDefaultIncludes(),
        ...options,
      });
    } catch (error) {
      this.logger.error('Error finding requisitions by status', {
        status,
        error: error.message,
      });
      throw error;
    }
  }

  getDefaultIncludes() {
    return [
      {
        model: this.models.User,
        as: 'requestor',
        attributes: ['id', 'firstName', 'lastName', 'email'],
      },
      {
        model: this.models.Company,
        as: 'company',
        attributes: ['id', 'name', 'code'],
      },
    ];
  }
}

Backend-Specific Standards

Testing

  • Write unit tests for all business logic
  • Write integration tests for API endpoints
  • Use mocks and stubs for dependencies
  • Follow the Arrange-Act-Assert pattern
  • Test both success and error cases
JavaScript
describe('UserService', () => {
  describe('createUser', () => {
    it('should create a user and send welcome email', async () => {
      // Arrange
      const userData = { email: 'test@example.com', name: 'Test User' };
      const createdUser = { id: 1, ...userData };

      userRepositoryMock.create.resolves(createdUser);
      emailServiceMock.sendWelcomeEmail.resolves();

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

      // Assert
      expect(result).to.deep.equal(createdUser);
      expect(userRepositoryMock.create.calledWith(userData)).to.be.true;
      expect(emailServiceMock.sendWelcomeEmail.calledWith(userData.email)).to.be.true;
    });

    it('should throw ValidationError if user data is invalid', async () => {
      // Arrange
      const userData = { name: 'Test User' }; // Missing email

      // Act & Assert
      await expect(userService.createUser(userData)).to.be.rejectedWith(ValidationError);
    });
  });
});

Frontend-Specific Standards

Component Structure

  • Use functional components with hooks
  • Keep components small and focused on a single responsibility
  • Use composition to build complex components
  • Extract reusable logic into custom hooks
  • Use prop types or TypeScript for type checking
JSX
// Good
function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <UserAvatar user={user} />
      <UserDetails user={user} />
    </div>
  );
}

// Bad
function UserProfile({ user }) {
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState({ ...user });

  // Too many responsibilities in one component
  // ...
}

React Hooks

  • Follow the Rules of Hooks
  • Keep hooks simple and focused
  • Use custom hooks to share logic between components
  • Use the appropriate hook for the task
JSX
// Good
function useUserData(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        const data = await api.getUser(userId);
        setUser(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]);

  return { user, loading, error };
}

// Bad
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Duplicate logic that should be in a custom hook
  useEffect(() => {
    async function fetchUser() {
      // ...
    }

    fetchUser();
  }, [userId]);

  // ...
}

State Management (PRS Patterns)

  • Use local state for component-specific state
  • Use Zustand for global state management
  • Use React Query for server state and caching
  • Follow established store patterns
JSX
// ✅ Correct - PRS Zustand store pattern
import { create } from 'zustand';

const useAuthStore = create((set, get) => ({
  // State
  token: null,
  type: null,
  refreshToken: null,
  expiredAt: null,

  // Actions
  setState: (newState) => set((state) => ({ ...state, ...newState })),

  clearAuth: () => set({
    token: null,
    type: null,
    refreshToken: null,
    expiredAt: null,
  }),

  // Getters
  isAuthenticated: () => {
    const { token, expiredAt } = get();
    return token && expiredAt && new Date() < new Date(expiredAt);
  },
}));

// ✅ Correct - Using the store in components
function AuthenticatedRoute({ children }) {
  const { isAuthenticated } = useAuthStore();

  if (!isAuthenticated()) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

API Integration Pattern

Follow the established API client pattern:

JSX
// ✅ Correct - Using the PRS API client
import { api } from '@/lib/api';
import { useNotification } from '@/hooks/useNotification';

function RequisitionForm() {
  const { showNotification } = useNotification();
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (data) => {
    try {
      setLoading(true);
      const response = await api.post('/requisitions', data);

      showNotification({
        type: 'success',
        message: 'Requisition created successfully',
      });

      // Handle success
    } catch (error) {
      showNotification({
        type: 'error',
        message: error.formattedError?.message || 'Failed to create requisition',
      });
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form content */}
    </form>
  );
}

Status Display Pattern

Use the established status component pattern:

JSX
// ✅ Correct - PRS Status component pattern
import { cn } from '@utils/cn';
import { cva } from 'class-variance-authority';

const statusStyles = cva(
  'py-2 w-full justify-center items-center inline-flex text-xs leading-5 font-bold rounded-full whitespace-pre-wrap capitalize',
  {
    variants: {
      status: {
        draft: 'bg-[#F5EAC5] text-[#D5AB18]',
        for_approval: 'bg-[#F0963D33] text-[#F0963D]',
        approved: 'bg-[#1EA52B33] text-[#1EA52B]',
        rejected: 'bg-[#DC433B33] text-[#DC433B]',
        // ... more status variants
      },
    },
    defaultVariants: {
      status: 'default',
    },
  },
);

const STATUSES = {
  draft: 'Draft',
  for_approval: 'For Approval',
  approved: 'Approved',
  rejected: 'Rejected',
  // ... more status mappings
};

function StatusBadge({ status, className }) {
  if (!status) return null;

  return (
    <span className={cn(statusStyles({ status }), className)}>
      {STATUSES[status] || status}
    </span>
  );
}

Styling

  • Use Tailwind CSS for styling
  • Follow a consistent naming convention for custom classes
  • Use the cn utility for conditional classes
  • Extract reusable styles into components
JSX
// Good
function Button({ variant = 'primary', size = 'md', children, className, ...props }) {
  return (
    <button
      className={cn(
        'rounded font-medium',
        {
          'bg-blue-500 text-white': variant === 'primary',
          'bg-gray-200 text-gray-800': variant === 'secondary',
          'px-2 py-1 text-sm': size === 'sm',
          'px-4 py-2': size === 'md',
          'px-6 py-3 text-lg': size === 'lg',
        },
        className
      )}
      {...props}
    >
      {children}
    </button>
  );
}

// Bad
function Button({ variant, children }) {
  let className = 'rounded font-medium';

  if (variant === 'primary') {
    className += ' bg-blue-500 text-white';
  } else {
    className += ' bg-gray-200 text-gray-800';
  }

  return (
    <button className={className}>
      {children}
    </button>
  );
}

Testing

  • Write unit tests for components using React Testing Library
  • Test component behavior, not implementation details
  • Write integration tests for complex workflows
  • Use mock service worker (MSW) for API mocking
JSX
// Good
import { render, screen, fireEvent } from '@testing-library/react';
import { UserProfile } from './UserProfile';

test('displays user information', () => {
  const user = { id: 1, name: 'John Doe', email: 'john@example.com' };

  render(<UserProfile user={user} />);

  expect(screen.getByText('John Doe')).toBeInTheDocument();
  expect(screen.getByText('john@example.com')).toBeInTheDocument();
});

test('shows edit form when edit button is clicked', () => {
  const user = { id: 1, name: 'John Doe', email: 'john@example.com' };

  render(<UserProfile user={user} />);

  fireEvent.click(screen.getByText('Edit'));

  expect(screen.getByLabelText('Name')).toHaveValue('John Doe');
  expect(screen.getByLabelText('Email')).toHaveValue('john@example.com');
});