elearning/.windsurf/rules/backenddevelopmentrules.md
2026-01-22 15:56:56 +07:00

11 KiB

trigger
manual

Backend Development Rules

Rules and guidelines for developing the E-Learning Platform Backend


🎯 General Principles

1. Code Organization

  • Follow MVC pattern: Models (Prisma) → Controllers → Routes
  • Keep controllers thin, business logic in services
  • One file per route group (e.g., auth.routes.js, courses.routes.js)
  • Maximum 200 lines per file (split if larger)

2. Naming Conventions

// Files: kebab-case
user-service.js
course-controller.js

// Functions: camelCase
getUserById()
createCourse()

// Classes: PascalCase
UserService
CourseController

// Constants: UPPER_SNAKE_CASE
MAX_FILE_SIZE
JWT_SECRET

// Database: snake_case
user_id
created_at

3. Multi-Language Support

ALWAYS use JSON structure for user-facing text:

// ✅ Correct
{
  title: { th: "หลักสูตร", en: "Course" }
}

// ❌ Wrong
{
  title: "หลักสูตร"
}

🔐 Security Rules

1. Authentication

  • ALWAYS validate JWT token in protected routes
  • NEVER trust client-side data
  • ALWAYS hash passwords with bcrypt (salt rounds: 10)
  • Token expiry: 24h (access), 7d (refresh)

2. Authorization

// Check authentication first
if (!req.user) return res.status(401).json({ error: "Unauthorized" });

// Then check role
if (!["ADMIN", "INSTRUCTOR"].includes(req.user.role.code)) {
  return res.status(403).json({ error: "Forbidden" });
}

// Finally check ownership
if (course.instructorId !== req.user.id && req.user.role.code !== "ADMIN") {
  return res.status(403).json({ error: "Forbidden" });
}

3. Input Validation

  • ALWAYS validate with Joi or Zod
  • NEVER trust user input
  • Sanitize HTML content
  • Validate file types and sizes

📁 File Upload Rules

1. File Validation

// Check file type
const allowedTypes = ['video/mp4', 'application/pdf', ...];
if (!allowedTypes.includes(file.mimetype)) {
  return error("Invalid file type");
}

// Check file size
if (file.size > MAX_FILE_SIZE) {
  return error("File too large");
}

2. File Storage (MinIO/S3)

// Use consistent path structure
const videoPath = `courses/${courseId}/lessons/${lessonId}/video.mp4`;
const attachmentPath = `lessons/${lessonId}/attachments/${uniqueFilename}`;

// Generate unique filenames
const uniqueFilename = `${Date.now()}-${uuidv4()}-${originalFilename}`;

3. File Limits

  • Video: 500 MB max
  • Attachment: 100 MB max per file
  • Max 10 attachments per lesson
  • Total 500 MB per lesson

🗄️ Database Rules

1. Prisma Best Practices

// ✅ Use transactions for related operations
await prisma.$transaction([
  prisma.lesson.create(...),
  prisma.attachment.createMany(...)
]);

// ✅ Include relations when needed
const course = await prisma.course.findUnique({
  where: { id },
  include: { chapters: { include: { lessons: true } } }
});

// ✅ Use select to limit fields
const user = await prisma.user.findUnique({
  where: { id },
  select: { id: true, username: true, email: true }
});

2. Soft Delete Pattern

// ✅ Use soft delete
await prisma.course.update({
  where: { id },
  data: { isDeleted: true, deletedAt: new Date() }
});

// ❌ Don't hard delete
await prisma.course.delete({ where: { id } }); // Avoid this

3. Query Optimization

  • Use select to fetch only needed fields
  • Use include wisely (avoid N+1 queries)
  • Add indexes for frequently queried fields
  • Use pagination for large datasets

🎯 API Response Format

1. Success Response

// Single resource
res.status(200).json({
  id: 1,
  title: { th: "...", en: "..." },
  ...
});

// List with pagination
res.status(200).json({
  data: [...],
  pagination: {
    page: 1,
    limit: 20,
    total: 150,
    totalPages: 8
  }
});

2. Error Response

res.status(400).json({
  error: {
    code: "VALIDATION_ERROR",
    message: "Invalid input data",
    details: [
      { field: "email", message: "Email is required" }
    ]
  }
});

3. Status Codes

  • 200 OK - Success
  • 201 Created - Resource created
  • 204 No Content - Success with no response body
  • 400 Bad Request - Validation error
  • 401 Unauthorized - Not authenticated
  • 403 Forbidden - Not authorized
  • 404 Not Found - Resource not found
  • 409 Conflict - Duplicate entry
  • 422 Unprocessable Entity - Business logic error
  • 500 Internal Server Error - Server error

🔄 Business Logic Rules

1. Course Management

  • New courses start as DRAFT
  • Must have ≥1 chapter and ≥3 lessons before submission
  • Only course owner or primary instructor can submit
  • Admin can approve/reject

2. Lesson Prerequisites

// Check prerequisites before allowing access
const canAccess = await checkLessonAccess(userId, lessonId);
if (!canAccess.allowed) {
  return res.status(403).json({
    error: {
      code: "LESSON_LOCKED",
      message: canAccess.reason,
      required_lessons: canAccess.requiredLessons
    }
  });
}

3. Quiz Attempts

// Validate before allowing quiz attempt
if (attempts >= quiz.maxAttempts) {
  return error("MAX_ATTEMPTS_EXCEEDED");
}

if (lastAttempt && (now - lastAttempt) < cooldown) {
  return error("COOLDOWN_ACTIVE");
}

4. Video Progress

  • Save progress every 5 seconds
  • Auto-complete at ≥90% watched
  • Use last_watched_at for concurrent sessions

5. Multi-Instructor

  • Cannot remove last instructor
  • Only primary instructor can manage other instructors
  • Course owner always has full access

🧪 Testing Rules

1. Test Coverage

  • Write tests for all business logic
  • Test happy path AND error cases
  • Test authentication and authorization
  • Test file uploads

2. Test Structure

describe('Course API', () => {
  describe('POST /api/instructor/courses', () => {
    it('should create course with valid data', async () => {
      // Arrange
      const courseData = {...};
      
      // Act
      const response = await request(app)
        .post('/api/instructor/courses')
        .set('Authorization', `Bearer ${token}`)
        .send(courseData);
      
      // Assert
      expect(response.status).toBe(201);
      expect(response.body).toHaveProperty('id');
    });
    
    it('should return 401 without authentication', async () => {
      // Test unauthorized access
    });
  });
});

📝 Code Documentation

1. JSDoc Comments

/**
 * Create a new course
 * @param {Object} req - Express request object
 * @param {Object} req.body - Course data
 * @param {Object} req.body.title - Course title (th/en)
 * @param {number} req.body.category_id - Category ID
 * @param {Object} res - Express response object
 * @returns {Promise<Object>} Created course
 */
async function createCourse(req, res) {
  // Implementation
}

2. Inline Comments

// Only for complex logic
// Explain WHY, not WHAT

// ✅ Good
// Use highest score to determine pass/fail (per quiz settings)
const finalScore = Math.max(...attemptScores);

// ❌ Bad
// Get max score
const finalScore = Math.max(...attemptScores);

🚨 Error Handling

1. Try-Catch Pattern

async function createCourse(req, res) {
  try {
    // Validate input
    const { error, value } = courseSchema.validate(req.body);
    if (error) {
      return res.status(400).json({
        error: {
          code: "VALIDATION_ERROR",
          message: error.details[0].message
        }
      });
    }
    
    // Business logic
    const course = await courseService.create(value, req.user.id);
    
    return res.status(201).json(course);
  } catch (err) {
    console.error('Create course error:', err);
    return res.status(500).json({
      error: {
        code: "INTERNAL_ERROR",
        message: "Failed to create course"
      }
    });
  }
}

2. Custom Error Classes

class ValidationError extends Error {
  constructor(message, details) {
    super(message);
    this.name = 'ValidationError';
    this.statusCode = 400;
    this.details = details;
  }
}

class UnauthorizedError extends Error {
  constructor(message = 'Unauthorized') {
    super(message);
    this.name = 'UnauthorizedError';
    this.statusCode = 401;
  }
}

🔍 Logging Rules

1. What to Log

// ✅ Log important events
logger.info('User logged in', { userId, timestamp });
logger.info('Course created', { courseId, instructorId });

// ✅ Log errors with context
logger.error('File upload failed', { 
  error: err.message, 
  userId, 
  filename 
});

// ❌ Don't log sensitive data
logger.info('User logged in', { password }); // NEVER!

2. Log Levels

  • error - Errors that need attention
  • warn - Warnings (deprecated features, etc.)
  • info - Important events (login, course creation)
  • debug - Detailed debugging (development only)

Performance Rules

1. Database Queries

// ✅ Use pagination
const courses = await prisma.course.findMany({
  skip: (page - 1) * limit,
  take: limit
});

// ✅ Limit relations depth
const course = await prisma.course.findUnique({
  include: {
    chapters: {
      include: { lessons: true }
      // Don't go deeper unless necessary
    }
  }
});

2. Caching (Redis)

// Cache frequently accessed data
const cacheKey = `course:${courseId}`;
const cached = await redis.get(cacheKey);

if (cached) {
  return JSON.parse(cached);
}

const course = await prisma.course.findUnique(...);
await redis.setex(cacheKey, 3600, JSON.stringify(course));
return course;

3. File Processing

// Process large files asynchronously
const job = await queue.add('process-video', {
  videoPath,
  lessonId
});

return res.status(202).json({
  message: 'Video processing started',
  jobId: job.id
});

🔒 Environment Variables

Required Variables

# Application
NODE_ENV=development
PORT=4000
APP_URL=http://localhost:4000

# Database
DATABASE_URL=postgresql://...

# Redis
REDIS_URL=redis://...

# MinIO/S3
S3_ENDPOINT=http://...
S3_ACCESS_KEY=...
S3_SECRET_KEY=...

# JWT
JWT_SECRET=...
JWT_EXPIRES_IN=24h

# Email (Mailhog in dev)
SMTP_HOST=...
SMTP_PORT=1025

Loading Environment Variables

// ✅ Use dotenv
require('dotenv').config();

// ✅ Validate required variables
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', ...];
requiredEnvVars.forEach(key => {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
});

📦 Dependencies

Core Dependencies

  • express - Web framework
  • @prisma/client - Database ORM
  • bcrypt - Password hashing
  • jsonwebtoken - JWT authentication
  • joi or zod - Input validation
  • multer - File uploads
  • aws-sdk or minio - S3 storage
  • redis - Caching
  • winston - Logging

Dev Dependencies

  • nodemon - Auto-restart
  • jest - Testing
  • supertest - API testing
  • eslint - Linting
  • prettier - Code formatting

🚀 Deployment Checklist

  • All environment variables set
  • Database migrations run
  • Redis connection tested
  • MinIO/S3 buckets created
  • JWT secret is strong and unique
  • CORS configured correctly
  • Rate limiting enabled
  • Logging configured
  • Error tracking setup (Sentry, etc.)
  • Health check endpoint working

Remember: Security first, code quality second, features third!