Skip to content

Coding Standards

Consistent coding standards improve readability, maintainability, and collaboration. These standards apply to all YeboLearn code.

Language Standards

TypeScript

Why TypeScript:

  • Type safety catches bugs at compile time
  • Better IDE support and autocomplete
  • Self-documenting code
  • Refactoring confidence

Type Safety Rules:

typescript
// DO: Use explicit types for function parameters and returns
export function calculateQuizScore(
  answers: Answer[],
  quiz: Quiz
): QuizResult {
  // Implementation
}

// DON'T: Use 'any' type
function processData(data: any) { // ❌
  // 'any' defeats purpose of TypeScript
}

// DO: Use proper types or 'unknown' with type guards
function processData(data: unknown) { // ✓
  if (isValidData(data)) {
    // TypeScript knows the type here
  }
}

Interface vs Type:

typescript
// DO: Use interfaces for object shapes (can be extended)
export interface Student {
  id: string;
  name: string;
  email: string;
  enrolledCourses: Course[];
}

// DO: Use types for unions, intersections, primitives
export type PaymentStatus = 'pending' | 'completed' | 'failed';
export type StudentWithProgress = Student & { progress: number };

// DON'T: Mix unnecessarily
type StudentType = { // ❌ Use interface
  id: string;
  name: string;
};

Null Safety:

typescript
// DO: Use optional chaining and nullish coalescing
const studentName = student?.profile?.name ?? 'Unknown';

// DO: Handle undefined/null explicitly
function getStudentGrade(studentId: string): number | null {
  const student = findStudent(studentId);
  if (!student) return null;
  return student.grade;
}

// DON'T: Use non-null assertion unless absolutely certain
const student = findStudent(id)!; // ❌ Can crash if null

Generics:

typescript
// DO: Use generics for reusable functions
export async function fetchData<T>(
  endpoint: string,
  validator: (data: unknown) => data is T
): Promise<T> {
  const response = await fetch(endpoint);
  const data = await response.json();

  if (!validator(data)) {
    throw new Error('Invalid data format');
  }

  return data;
}

// Usage with type safety
const students = await fetchData<Student[]>(
  '/api/students',
  isStudentArray
);

Enums vs Union Types:

typescript
// DO: Use union types (more flexible)
export type QuizDifficulty = 'easy' | 'medium' | 'hard';

// PREFER: Union types over enums
const difficulty: QuizDifficulty = 'medium'; // ✓

// AVOID: Enums (unless you need reverse mapping)
enum QuizDifficulty { // ❌ Less flexible
  Easy = 'easy',
  Medium = 'medium',
  Hard = 'hard',
}

React Standards

Component Structure:

typescript
// DO: Functional components with TypeScript
import { useState, useEffect } from 'react';

interface QuizCardProps {
  quiz: Quiz;
  onStart: (quizId: string) => void;
  showProgress?: boolean;
}

export function QuizCard({
  quiz,
  onStart,
  showProgress = true
}: QuizCardProps) {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      className="quiz-card"
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      <h3>{quiz.title}</h3>
      {showProgress && <ProgressBar progress={quiz.progress} />}
      <button onClick={() => onStart(quiz.id)}>Start Quiz</button>
    </div>
  );
}

// DON'T: Class components (legacy pattern)
class QuizCard extends React.Component { // ❌
  // Prefer functional components
}

Hooks Best Practices:

typescript
// DO: Custom hooks for reusable logic
export function useQuizProgress(quizId: string) {
  const [progress, setProgress] = useState(0);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function loadProgress() {
      setIsLoading(true);
      const data = await fetchQuizProgress(quizId);
      setProgress(data.progress);
      setIsLoading(false);
    }
    loadProgress();
  }, [quizId]);

  return { progress, isLoading };
}

// Usage
function QuizDashboard({ quizId }: { quizId: string }) {
  const { progress, isLoading } = useQuizProgress(quizId);

  if (isLoading) return <Loading />;
  return <ProgressBar progress={progress} />;
}

// DON'T: Put complex logic directly in components
function QuizDashboard({ quizId }: { quizId: string }) { // ❌
  const [progress, setProgress] = useState(0);
  useEffect(() => {
    // 50 lines of logic here...
  }, [quizId]);
  // Extract to custom hook!
}

State Management:

typescript
// DO: Keep state close to where it's used
function QuizQuestion({ question }: { question: Question }) {
  const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null);
  // Local state for this component only
}

// DO: Lift state when shared across components
function QuizPage() {
  const [answers, setAnswers] = useState<Record<string, string>>({});

  return (
    <>
      {questions.map(q => (
        <QuizQuestion
          key={q.id}
          question={q}
          selectedAnswer={answers[q.id]}
          onAnswerSelect={(answer) =>
            setAnswers(prev => ({ ...prev, [q.id]: answer }))
          }
        />
      ))}
    </>
  );
}

// DO: Use Context for deeply nested shared state
const AuthContext = createContext<AuthState | null>(null);

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

Component Naming:

typescript
// DO: PascalCase for components
export function StudentDashboard() { }
export function AIQuizGenerator() { }

// DO: Descriptive names that indicate purpose
export function QuizSubmitButton() { } // ✓
export function Button() { } // ❌ Too generic

// DO: Suffix containers with 'Container' or 'Page'
export function CourseListContainer() { }
export function DashboardPage() { }

Node.js/API Standards

API Route Structure:

typescript
// DO: RESTful routes with proper HTTP methods
import { Router } from 'express';
import { authenticate, authorize } from '../middleware/auth';
import { validateRequest } from '../middleware/validation';
import { quizSchema } from '../schemas';

const router = Router();

// GET /api/quizzes - List quizzes
router.get('/quizzes',
  authenticate,
  async (req, res) => {
    const quizzes = await db.quiz.findMany({
      where: { published: true },
    });
    res.json({ data: quizzes });
  }
);

// POST /api/quizzes - Create quiz
router.post('/quizzes',
  authenticate,
  authorize('teacher'),
  validateRequest(quizSchema),
  async (req, res) => {
    const quiz = await db.quiz.create({
      data: req.body,
    });
    res.status(201).json({ data: quiz });
  }
);

// GET /api/quizzes/:id - Get single quiz
router.get('/quizzes/:id',
  authenticate,
  async (req, res) => {
    const quiz = await db.quiz.findUnique({
      where: { id: req.params.id },
    });

    if (!quiz) {
      return res.status(404).json({
        error: 'Quiz not found'
      });
    }

    res.json({ data: quiz });
  }
);

export default router;

Error Handling:

typescript
// DO: Use custom error classes
export class NotFoundError extends Error {
  statusCode = 404;
  constructor(message: string) {
    super(message);
    this.name = 'NotFoundError';
  }
}

export class ValidationError extends Error {
  statusCode = 400;
  constructor(public errors: ValidationErrorDetail[]) {
    super('Validation failed');
    this.name = 'ValidationError';
  }
}

// DO: Centralized error handler
export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Log error
  logger.error('Request failed', {
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
  });

  // Send appropriate response
  if (err instanceof ValidationError) {
    return res.status(400).json({
      error: 'Validation failed',
      details: err.errors,
    });
  }

  if (err instanceof NotFoundError) {
    return res.status(404).json({
      error: err.message,
    });
  }

  // Default 500 error
  res.status(500).json({
    error: 'Internal server error',
  });
}

// Usage in routes
async function getQuiz(req: Request, res: Response) {
  const quiz = await db.quiz.findUnique({
    where: { id: req.params.id },
  });

  if (!quiz) {
    throw new NotFoundError('Quiz not found');
  }

  res.json({ data: quiz });
}

Async/Await:

typescript
// DO: Use async/await consistently
export async function createQuiz(data: QuizData): Promise<Quiz> {
  // Validate data
  const validated = await validateQuizData(data);

  // Create quiz
  const quiz = await db.quiz.create({
    data: validated,
  });

  // Send notification
  await notifyTeacher(quiz.teacherId, quiz.id);

  return quiz;
}

// DON'T: Mix promises and async/await
export function createQuiz(data: QuizData): Promise<Quiz> { // ❌
  return validateQuizData(data).then(validated => {
    return db.quiz.create({ data: validated }).then(quiz => {
      notifyTeacher(quiz.teacherId, quiz.id);
      return quiz;
    });
  });
}

// DO: Handle errors properly
export async function createQuiz(data: QuizData): Promise<Quiz> {
  try {
    const validated = await validateQuizData(data);
    const quiz = await db.quiz.create({ data: validated });
    await notifyTeacher(quiz.teacherId, quiz.id);
    return quiz;
  } catch (error) {
    logger.error('Failed to create quiz', { error, data });
    throw new QuizCreationError('Could not create quiz');
  }
}

Code Organization

File Structure:

src/
├── api/                    # API routes
│   ├── routes/
│   │   ├── quizzes.ts
│   │   ├── students.ts
│   │   └── auth.ts
│   ├── middleware/
│   │   ├── auth.ts
│   │   ├── validation.ts
│   │   └── rateLimit.ts
│   └── schemas/
│       └── quiz.schema.ts
├── features/               # Feature modules
│   ├── ai/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── services/
│   │   └── types.ts
│   └── quiz/
│       ├── components/
│       ├── hooks/
│       └── types.ts
├── shared/                 # Shared utilities
│   ├── components/
│   ├── hooks/
│   ├── utils/
│   └── types/
├── lib/                    # Third-party integrations
│   ├── database.ts
│   ├── gemini.ts
│   └── email.ts
└── config/                 # Configuration
    ├── env.ts
    └── constants.ts

Import Organization:**

typescript
// DO: Group imports by source
// 1. External libraries
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';

// 2. Internal modules (absolute imports)
import { Button } from '@/shared/components/Button';
import { useAuth } from '@/features/auth/hooks/useAuth';
import { QuizService } from '@/features/quiz/services/QuizService';

// 3. Types
import type { Quiz, QuizResult } from '@/features/quiz/types';

// 4. Styles (if applicable)
import styles from './QuizPage.module.css';

// DON'T: Mix import sources randomly
import { useAuth } from '@/features/auth/hooks/useAuth'; // ❌
import React from 'react';
import type { Quiz } from '@/features/quiz/types';
import { Button } from '@/shared/components/Button';

Naming Conventions

Variables:

typescript
// DO: camelCase for variables and functions
const studentCount = 42;
const quizResults = [];
function calculateScore() { }

// DO: UPPER_SNAKE_CASE for constants
const MAX_QUIZ_ATTEMPTS = 3;
const API_BASE_URL = 'https://api.yebolearn.app';

// DO: Descriptive names
const activeStudentCount = 42; // ✓
const count = 42; // ❌ What count?

// DO: Boolean names that read like questions
const isAuthenticated = true;
const hasCompletedQuiz = false;
const canSubmitAnswer = true;

// DON'T: Vague boolean names
const authenticated = true; // ❌
const quiz = false; // ❌ What does this mean?

Functions:

typescript
// DO: Verb-based names for actions
function createQuiz() { }
function deleteStudent() { }
function updateProgress() { }

// DO: Question format for boolean returns
function isValidEmail(email: string): boolean { }
function hasPermission(user: User, action: string): boolean { }

// DO: get/set for getters/setters
function getStudentById(id: string): Student | null { }
function setQuizTitle(quiz: Quiz, title: string): void { }

// DO: handle/on prefix for event handlers
function handleSubmit(event: FormEvent) { }
function onQuizComplete(result: QuizResult) { }

Linting and Formatting

ESLint Configuration:

javascript
// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
  ],
  rules: {
    // Enforce
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unused-vars': 'error',
    'react/prop-types': 'off', // Using TypeScript
    'react/react-in-jsx-scope': 'off', // Next.js handles this

    // Warn (will fix eventually)
    '@typescript-eslint/no-non-null-assertion': 'warn',
    'no-console': 'warn',

    // Disable
    'react/display-name': 'off',
  },
};

Prettier Configuration:

javascript
// .prettierrc.js
module.exports = {
  semi: true,
  trailingComma: 'es5',
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  arrowParens: 'avoid',
};

Pre-commit Hooks:

json
// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

Code Review Checklist

For Authors:

Before Creating PR:

  • [ ] Code is self-reviewed
  • [ ] All tests pass locally
  • [ ] Linting and formatting applied
  • [ ] No console.log or debug code
  • [ ] Comments explain "why", not "what"
  • [ ] Complex logic has JSDoc comments

PR Description Includes:

  • [ ] What changed and why
  • [ ] Testing performed
  • [ ] Screenshots (if UI changes)
  • [ ] Breaking changes noted
  • [ ] Related issues linked

For Reviewers:

Code Quality:

  • [ ] Code is readable and maintainable
  • [ ] No obvious bugs or edge cases missed
  • [ ] Error handling is comprehensive
  • [ ] No code duplication (DRY principle)
  • [ ] Appropriate use of abstractions

Testing:

  • [ ] Tests cover new functionality
  • [ ] Tests are meaningful (not just for coverage)
  • [ ] Edge cases tested
  • [ ] Error scenarios tested

Performance:

  • [ ] No unnecessary re-renders (React)
  • [ ] Database queries optimized
  • [ ] No N+1 query problems
  • [ ] Large lists virtualized
  • [ ] Images optimized

Security:

  • [ ] No SQL injection risks
  • [ ] No XSS vulnerabilities
  • [ ] Input validation present
  • [ ] Authentication checked
  • [ ] Authorization verified
  • [ ] No secrets in code

Best Practices:

  • [ ] Follows coding standards
  • [ ] Uses TypeScript properly
  • [ ] Consistent with existing patterns
  • [ ] Documentation updated
  • [ ] No breaking changes (or documented)

Documentation Standards

Code Comments:

typescript
// DO: Explain complex logic
/**
 * Calculates quiz score using weighted rubric.
 *
 * Each question type has different weight:
 * - Multiple choice: 1 point
 * - Short answer: 2 points
 * - Essay: 5 points
 *
 * Score is normalized to 0-100 scale.
 */
export function calculateQuizScore(
  answers: Answer[],
  rubric: Rubric
): number {
  // Implementation
}

// DO: Explain non-obvious decisions
// Using exponential backoff to avoid overwhelming the API
// during high traffic periods
const retryDelay = Math.pow(2, attemptCount) * 1000;

// DON'T: State the obvious
const count = 0; // Initialize count to 0 ❌

JSDoc for Public APIs:

typescript
/**
 * Generates an AI-powered quiz based on course content.
 *
 * @param topic - The topic for the quiz
 * @param difficulty - Quiz difficulty level
 * @param questionCount - Number of questions to generate
 * @returns Promise resolving to generated quiz
 * @throws {RateLimitError} If API rate limit exceeded
 * @throws {ValidationError} If parameters are invalid
 *
 * @example
 * ```typescript
 * const quiz = await generateQuiz('Photosynthesis', 'medium', 10);
 * console.log(quiz.questions.length); // 10
 * ```
 */
export async function generateQuiz(
  topic: string,
  difficulty: QuizDifficulty,
  questionCount: number
): Promise<Quiz> {
  // Implementation
}

Best Practices

General Principles:

SOLID Principles:

  • Single Responsibility: One function, one purpose
  • Open/Closed: Open for extension, closed for modification
  • Liskov Substitution: Derived types must be substitutable
  • Interface Segregation: Many specific interfaces > one general
  • Dependency Inversion: Depend on abstractions, not concretions

DRY (Don't Repeat Yourself):

typescript
// DON'T: Duplicate logic
function getStudentQuizScore(studentId: string) { // ❌
  const student = await db.student.findUnique({ where: { id: studentId } });
  const attempts = await db.quizAttempt.findMany({ where: { studentId } });
  return attempts.reduce((sum, a) => sum + a.score, 0) / attempts.length;
}

function getStudentOverallScore(studentId: string) { // ❌
  const student = await db.student.findUnique({ where: { id: studentId } });
  const attempts = await db.quizAttempt.findMany({ where: { studentId } });
  return attempts.reduce((sum, a) => sum + a.score, 0) / attempts.length;
}

// DO: Extract shared logic
async function getAverageScore(attempts: QuizAttempt[]): Promise<number> {
  if (attempts.length === 0) return 0;
  const total = attempts.reduce((sum, a) => sum + a.score, 0);
  return total / attempts.length;
}

async function getStudentQuizScore(studentId: string) {
  const attempts = await db.quizAttempt.findMany({ where: { studentId } });
  return getAverageScore(attempts);
}

KISS (Keep It Simple):

typescript
// DON'T: Overcomplicate
function isEligibleForQuiz(student: Student): boolean { // ❌
  return student.enrollments
    .filter(e => e.status === 'active')
    .map(e => e.course)
    .flatMap(c => c.quizzes)
    .some(q => !student.attempts.some(a => a.quizId === q.id));
}

// DO: Keep it simple
function isEligibleForQuiz(student: Student): boolean { // ✓
  const hasActiveEnrollment = student.enrollments.some(
    e => e.status === 'active'
  );
  const hasIncompleteQuizzes = student.availableQuizzes.length > 0;
  return hasActiveEnrollment && hasIncompleteQuizzes;
}

YAGNI (You Aren't Gonna Need It):

typescript
// DON'T: Build for hypothetical future
class QuizService { // ❌
  async createQuiz() { }
  async updateQuiz() { }
  async deleteQuiz() { }
  async archiveQuiz() { } // Not needed yet
  async unarchiveQuiz() { } // Not needed yet
  async duplicateQuiz() { } // Not needed yet
  async exportQuiz() { } // Not needed yet
  async importQuiz() { } // Not needed yet
}

// DO: Build what's needed now
class QuizService { // ✓
  async createQuiz() { }
  async updateQuiz() { }
  async deleteQuiz() { }
  // Add other methods when actually needed
}

YeboLearn - Empowering African Education