550 lines
11 KiB
Markdown
550 lines
11 KiB
Markdown
|
|
---
|
||
|
|
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
|
||
|
|
```javascript
|
||
|
|
// 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:
|
||
|
|
```javascript
|
||
|
|
// ✅ 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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
// 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)
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
// ✅ 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
|
||
|
|
```javascript
|
||
|
|
// ✅ 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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
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
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* 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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
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
|
||
|
|
```javascript
|
||
|
|
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
|
||
|
|
```javascript
|
||
|
|
// ✅ 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
|
||
|
|
```javascript
|
||
|
|
// ✅ 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)
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```javascript
|
||
|
|
// 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
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
```javascript
|
||
|
|
// ✅ 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!
|