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 nullGenerics:
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.tsImport 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
}Related Documentation
- Quality Overview - Quality standards and metrics
- Monitoring - Application monitoring
- Testing Strategy - Testing approach
- Code Review Process - PR workflow