11 KiB
11 KiB
| trigger |
|---|
| always_on |
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
selectto fetch only needed fields - Use
includewisely (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
200OK - Success201Created - Resource created204No Content - Success with no response body400Bad Request - Validation error401Unauthorized - Not authenticated403Forbidden - Not authorized404Not Found - Resource not found409Conflict - Duplicate entry422Unprocessable Entity - Business logic error500Internal 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_atfor 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 attentionwarn- 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 ORMbcrypt- Password hashingjsonwebtoken- JWT authenticationjoiorzod- Input validationmulter- File uploadsaws-sdkorminio- S3 storageredis- Cachingwinston- Logging
Dev Dependencies
nodemon- Auto-restartjest- Testingsupertest- API testingeslint- Lintingprettier- 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!