# API Structure Guide - Curriculum Management ## 📋 Tổng quan API được tổ chức theo cấu trúc phân cấp (hierarchy) và quan hệ nhiều-nhiều: ### Cấu trúc phân cấp (1:N): ``` Category (Danh mục) └── Subject (Môn học) └── Chapter (Chương) └── Lesson (Bài học) ``` ### Quan hệ nhiều-nhiều (N:N): ``` Lesson (Bài học) ←→ Story (Câu chuyện) - Một lesson có thể có nhiều stories - Một story có thể được sử dụng trong nhiều lessons - Quản lý qua bảng pivot: lesson_stories ``` ## 🎯 Nguyên tắc thiết kế ### ✅ Điều NÊN làm: 1. **Giữ nguyên cấu trúc phân tách**: Mỗi resource (category, subject, chapter, lesson) có router và controller riêng 2. **Sử dụng nested routes**: Cho các thao tác liên quan đến parent-child relationship 3. **RESTful naming**: Đặt tên routes theo chuẩn REST 4. **Validation đầy đủ**: Kiểm tra parent resource tồn tại trước khi thao tác ### ❌ Điều KHÔNG NÊN làm: 1. **Không gộp chung tất cả vào 1 file**: Vi phạm Single Responsibility Principle 2. **Không skip validation**: Luôn kiểm tra parent resource 3. **Không quên clear cache**: Cache phải được xóa khi có thay đổi --- ## 📚 API Endpoints ### 1. Categories API #### CRUD Operations ```http GET /api/categories # Lấy danh sách categories POST /api/categories # Tạo category mới GET /api/categories/:id # Lấy chi tiết category PUT /api/categories/:id # Cập nhật category DELETE /api/categories/:id # Xóa category ``` #### Nested Subject Operations ```http GET /api/categories/:categoryId/subjects # Lấy subjects trong category POST /api/categories/:categoryId/subjects # Thêm subject vào category DELETE /api/categories/:categoryId/subjects/:subjectId # Xóa subject khỏi category ``` **Ví dụ sử dụng:** ```bash # Tạo subject mới trong category curl -X POST http://localhost:3000/api/categories/123e4567-e89b-12d3-a456-426614174000/subjects \ -H "Content-Type: application/json" \ -d '{ "subject_code": "ENG101", "subject_name": "English Basics", "description": "Basic English course" }' # Xóa subject khỏi category curl -X DELETE http://localhost:3000/api/categories/123e4567-e89b-12d3-a456-426614174000/subjects/456e7890-e89b-12d3-a456-426614174001 ``` --- ### 2. Subjects API #### CRUD Operations ```http GET /api/subjects # Lấy danh sách subjects POST /api/subjects # Tạo subject mới GET /api/subjects/:id # Lấy chi tiết subject PUT /api/subjects/:id # Cập nhật subject DELETE /api/subjects/:id # Xóa subject ``` #### Nested Chapter Operations ```http GET /api/subjects/:subjectId/chapters # Lấy chapters trong subject POST /api/subjects/:subjectId/chapters # Thêm chapter vào subject DELETE /api/subjects/:subjectId/chapters/:chapterId # Xóa chapter khỏi subject ``` **Ví dụ sử dụng:** ```bash # Tạo chapter mới trong subject curl -X POST http://localhost:3000/api/subjects/456e7890-e89b-12d3-a456-426614174001/chapters \ -H "Content-Type: application/json" \ -d '{ "chapter_number": 1, "chapter_title": "Introduction", "chapter_description": "Getting started with English", "display_order": 1 }' # Xóa chapter khỏi subject curl -X DELETE http://localhost:3000/api/subjects/456e7890-e89b-12d3-a456-426614174001/chapters/789e0123-e89b-12d3-a456-426614174002 ``` --- ### 3. Chapters API #### CRUD Operations ```http GET /api/chapters # Lấy danh sách chapters POST /api/chapters # Tạo chapter mới GET /api/chapters/:id # Lấy chi tiết chapter PUT /api/chapters/:id # Cập nhật chapter DELETE /api/chapters/:id # Xóa chapter ``` #### Nested Lesson Operations ```http GET /api/chapters/:chapterId/lessons # Lấy lessons trong chapter POST /api/chapters/:chapterId/lessons # Thêm lesson vào chapter DELETE /api/chapters/:chapterId/lessons/:lessonId # Xóa lesson khỏi chapter ``` **Ví dụ sử dụng:** ```bash # Tạo lesson mới trong chapter curl -X POST http://localhost:3000/api/chapters/789e0123-e89b-12d3-a456-426614174002/lessons \ -H "Content-Type: application/json" \ -d '{ "lesson_number": 1, "lesson_title": "Alphabet A-F", "lesson_type": "json_content", "display_order": 1 }' # Xóa lesson khỏi chapter curl -X DELETE http://localhost:3000/api/chapters/789e0123-e89b-12d3-a456-426614174002/lessons/012e3456-e89b-12d3-a456-426614174003 ``` --- ### 4. Lessons API #### CRUD Operations ```http GET /api/lessons # Lấy danh sách lessons POST /api/lessons # Tạo lesson mới (cần chỉ định chapter_id) GET /api/lessons/:id # Lấy chi tiết lesson PUT /api/lessons/:id # Cập nhật lesson DELETE /api/lessons/:id # Xóa lesson ``` #### Nested Story Operations (N:N Relationship) ```http GET /api/lessons/:lessonId/stories # Lấy stories trong lesson POST /api/lessons/:lessonId/stories # Thêm story vào lesson PUT /api/lessons/:lessonId/stories/:storyId # Cập nhật story trong lesson DELETE /api/lessons/:lessonId/stories/:storyId # Xóa story khỏi lesson ``` **Ví dụ sử dụng:** ```bash # Thêm story vào lesson curl -X POST http://localhost:3000/api/lessons/012e3456-e89b-12d3-a456-426614174003/stories \ -H "Content-Type: application/json" \ -d '{ "story_id": "234e5678-e89b-12d3-a456-426614174004", "display_order": 1, "is_required": true }' # Cập nhật story trong lesson curl -X PUT http://localhost:3000/api/lessons/012e3456-e89b-12d3-a456-426614174003/stories/234e5678-e89b-12d3-a456-426614174004 \ -H "Content-Type: application/json" \ -d '{ "display_order": 2, "is_required": false }' # Xóa story khỏi lesson curl -X DELETE http://localhost:3000/api/lessons/012e3456-e89b-12d3-a456-426614174003/stories/234e5678-e89b-12d3-a456-426614174004 ``` --- ### 5. Stories API #### CRUD Operations ```http GET /api/stories # Lấy danh sách stories POST /api/stories # Tạo story mới GET /api/stories/:id # Lấy chi tiết story PUT /api/stories/:id # Cập nhật story DELETE /api/stories/:id # Xóa story ``` #### Nested Lesson Operations (N:N Relationship - Alternative Way) ```http GET /api/stories/:storyId/lessons # Lấy lessons sử dụng story này POST /api/stories/:storyId/lessons # Thêm lesson vào story DELETE /api/stories/:storyId/lessons/:lessonId # Xóa lesson khỏi story ``` **Ví dụ sử dụng:** ```bash # Lấy danh sách lessons sử dụng story curl -X GET http://localhost:3000/api/stories/234e5678-e89b-12d3-a456-426614174004/lessons # Thêm lesson vào story (cách alternative) curl -X POST http://localhost:3000/api/stories/234e5678-e89b-12d3-a456-426614174004/lessons \ -H "Content-Type: application/json" \ -d '{ "lesson_id": "012e3456-e89b-12d3-a456-426614174003", "display_order": 1, "is_required": true }' ``` --- ## 🔄 So sánh 2 cách tiếp cận ### Cách 1: Nested Routes (✅ Được áp dụng) #### Quan hệ 1:N (Parent-Child) ```http POST /api/categories/:categoryId/subjects POST /api/subjects/:subjectId/chapters POST /api/chapters/:chapterId/lessons ``` #### Quan hệ N:N (Many-to-Many) ```http # Từ phía Lesson POST /api/lessons/:lessonId/stories GET /api/lessons/:lessonId/stories PUT /api/lessons/:lessonId/stories/:storyId DELETE /api/lessons/:lessonId/stories/:storyId # Từ phía Story (alternative) POST /api/stories/:storyId/lessons GET /api/stories/:storyId/lessons DELETE /api/stories/:storyId/lessons/:lessonId ``` **Ưu điểm:** - ✅ Rõ ràng về mối quan hệ parent-child và many-to-many - ✅ Tự động validate parent tồn tại - ✅ RESTful và semantic - ✅ Dễ hiểu cho developers - ✅ Hỗ trợ quản lý pivot table metadata (display_order, is_required) **Nhược điểm:** - ⚠️ URL có thể dài hơn - ⚠️ Cần nhiều routes hơn ### Cách 2: Flat Routes ```http POST /api/subjects (với category_id trong body) POST /api/chapters (với subject_id trong body) POST /api/lessons (với chapter_id trong body) ``` **Ưu điểm:** - ✅ URL ngắn hơn - ✅ Ít routes hơn **Nhược điểm:** - ❌ Không rõ ràng về mối quan hệ - ❌ Phải validate parent manually trong body - ❌ Kém RESTful --- ## 🎨 Patterns được áp dụng ### 1. Controller Pattern Mỗi resource có controller riêng: ``` controllers/ ├── categoryController.js # Quản lý Categories + nested Subjects ├── subjectController.js # Quản lý Subjects + nested Chapters ├── chapterController.js # Quản lý Chapters + nested Lessons ├── lessonController.js # Quản lý Lessons + nested Stories (N:N) └── storyController.js # Quản lý Stories + nested Lessons (N:N) ``` ### 2. Route Organization Mỗi resource có route file riêng: ``` routes/ ├── categoryRoutes.js # Category routes + nested Subject routes ├── subjectRoutes.js # Subject routes + nested Chapter routes ├── chapterRoutes.js # Chapter routes + nested Lesson routes ├── lessonRoutes.js # Lesson routes + nested Story routes └── storyRoutes.js # Story routes + nested Lesson routes ``` ### 3. Model Relationships ```javascript // 1:N Relationships Category.hasMany(Subject) Subject.hasMany(Chapter) Chapter.hasMany(Lesson) // N:N Relationship Lesson.belongsToMany(Story, { through: 'LessonStory' }) Story.belongsToMany(Lesson, { through: 'LessonStory' }) ``` ### 4. Validation Pattern ```javascript // Luôn validate parent trước khi thao tác child const parent = await ParentModel.findByPk(parentId); if (!parent) { return res.status(404).json({ success: false, message: 'Parent not found' }); } // Validate cả 2 phía trong quan hệ N:N const lesson = await Lesson.findByPk(lessonId); const story = await Story.findByPk(storyId); if (!lesson || !story) { return res.status(404).json({ success: false, message: 'Lesson or Story not found' }); } // Kiểm tra duplicate trong quan hệ N:N const existing = await LessonStory.findOne({ where: { lesson_id: lessonId, story_id: storyId } }); if (existing) { return res.status(400).json({ success: false, message: 'Relationship already exists' }); } ``` ### 5. Cache Invalidation Pattern ```javascript // Xóa cache của cả parent và child (1:N) await cacheUtils.deletePattern(`parent:${parentId}:children:*`); await cacheUtils.deletePattern('children:list:*'); // Xóa cache của cả 2 phía (N:N) await cacheUtils.deletePattern(`lesson:${lessonId}:stories:*`); await cacheUtils.deletePattern(`story:${storyId}:lessons:*`); ``` ### 6. Pivot Table Pattern (N:N) ```javascript // Bảng LessonStory với metadata { lesson_id: UUID, story_id: UUID, display_order: INTEGER, // Thứ tự hiển thị is_required: BOOLEAN, // Có bắt buộc không } // Truy vấn với pivot data const stories = await lesson.getStories({ joinTableAttributes: ['display_order', 'is_required'] }); ``` --- ## 🔍 Best Practices ### 1. Đặt tên Routes ```javascript // ✅ Đúng: RESTful naming router.post('/:categoryId/subjects', ...) router.delete('/:categoryId/subjects/:subjectId', ...) // ❌ Sai: Naming không chuẩn router.post('/:categoryId/addSubject', ...) router.delete('/:categoryId/removeSubject/:subjectId', ...) ``` ### 2. Response Structure ```javascript // ✅ Đúng: Consistent response structure { "success": true, "message": "Subject added to category successfully", "data": { ...subject } } // ❌ Sai: Inconsistent { "subject": { ...subject }, "msg": "OK" } ``` ### 3. Error Handling ```javascript // ✅ Đúng: Descriptive error messages if (!chapter) { return res.status(404).json({ success: false, message: 'Chapter not found in this subject' }); } // ❌ Sai: Generic error if (!chapter) { return res.status(404).json({ error: 'Not found' }); } ``` --- ## 📊 Request/Response Examples ### Example 1: Tạo Subject trong Category **Request:** ```http POST /api/categories/123e4567-e89b-12d3-a456-426614174000/subjects Content-Type: application/json { "subject_code": "ENG101", "subject_name": "English Basics", "subject_name_en": "English Basics", "description": "Basic English for beginners", "is_active": true, "is_premium": false } ``` **Response:** ```json { "success": true, "message": "Subject added to category successfully", "data": { "id": "456e7890-e89b-12d3-a456-426614174001", "category_id": "123e4567-e89b-12d3-a456-426614174000", "subject_code": "ENG101", "subject_name": "English Basics", "subject_name_en": "English Basics", "description": "Basic English for beginners", "is_active": true, "is_premium": false, "created_at": "2026-02-26T10:00:00.000Z", "updated_at": "2026-02-26T10:00:00.000Z" } } ``` ### Example 2: Xóa Chapter khỏi Subject **Request:** ```http DELETE /api/subjects/456e7890-e89b-12d3-a456-426614174001/chapters/789e0123-e89b-12d3-a456-426614174002 ``` **Response (Success):** ```json { "success": true, "message": "Chapter removed from subject successfully" } ``` **Response (Error - Chapter có lessons):** ```json { "success": false, "message": "Cannot delete chapter. It has 5 lesson(s). Delete lessons first." } ``` ### Example 3: Thêm Story vào Lesson (N:N) **Request:** ```http POST /api/lessons/012e3456-e89b-12d3-a456-426614174003/stories Content-Type: application/json { "story_id": "234e5678-e89b-12d3-a456-426614174004", "display_order": 1, "is_required": true } ``` **Response:** ```json { "success": true, "message": "Story đã được thêm vào bài học", "data": { "id": "345e6789-e89b-12d3-a456-426614174005", "lesson_id": "012e3456-e89b-12d3-a456-426614174003", "story_id": "234e5678-e89b-12d3-a456-426614174004", "display_order": 1, "is_required": true, "created_at": "2026-02-26T11:00:00.000Z", "updated_at": "2026-02-26T11:00:00.000Z" } } ``` ### Example 4: Lấy Stories trong Lesson **Request:** ```http GET /api/lessons/012e3456-e89b-12d3-a456-426614174003/stories?page=1&limit=10 ``` **Response:** ```json { "success": true, "data": { "lesson": { "id": "012e3456-e89b-12d3-a456-426614174003", "lesson_title": "Alphabet A-F", "lesson_number": 1 }, "stories": [ { "id": "234e5678-e89b-12d3-a456-426614174004", "name": "The Greedy Cat", "type": "story", "thumbnail": "https://cdn.sena.tech/thumbs/greedy-cat.jpg", "display_order": 1, "is_required": true } ], "pagination": { "total": 1, "page": 1, "limit": 10, "totalPages": 1 } }, "cached": false } ``` ### Example 5: Cập nhật Story trong Lesson **Request:** ```http PUT /api/lessons/012e3456-e89b-12d3-a456-426614174003/stories/234e5678-e89b-12d3-a456-426614174004 Content-Type: application/json { "display_order": 2, "is_required": false } ``` **Response:** ```json { "success": true, "message": "Cập nhật thành công", "data": { "id": "345e6789-e89b-12d3-a456-426614174005", "lesson_id": "012e3456-e89b-12d3-a456-426614174003", "story_id": "234e5678-e89b-12d3-a456-426614174004", "display_order": 2, "is_required": false, "updated_at": "2026-02-26T12:00:00.000Z" } } ``` --- ## 🚀 Migration Guide Nếu bạn đang sử dụng cấu trúc cũ, đây là cách migrate: ### Before (Cũ) ```javascript // Tạo subject POST /api/subjects Body: { category_id: "xxx", subject_code: "ENG101", ... } // Tạo chapter POST /api/chapters Body: { subject_id: "yyy", chapter_number: 1, ... } ``` ### After (Mới) ```javascript // Cách 1: Sử dụng nested routes (khuyến nghị) POST /api/categories/:categoryId/subjects Body: { subject_code: "ENG101", ... } // Không cần category_id POST /api/subjects/:subjectId/chapters Body: { chapter_number: 1, ... } // Không cần subject_id // Cách 2: Vẫn dùng flat routes (vẫn hỗ trợ) POST /api/subjects Body: { category_id: "xxx", subject_code: "ENG101", ... } POST /api/chapters Body: { subject_id: "yyy", chapter_number: 1, ... } ``` --- ## 📝 Notes 1. **Backward Compatibility**: Flat routes vẫn hoạt động bình thường 2. **Nested routes**: Được khuyến nghị cho các thao tác mới 3. **Cache Strategy**: Automatic cache invalidation được implement ở tất cả endpoints 4. **Validation**: Parent resource luôn được validate trước khi thao tác --- ## 🔗 Related Documents - [SUBJECT_CHAPTER_LESSON_GUIDE.md](SUBJECT_CHAPTER_LESSON_GUIDE.md) - [LEARNING_CONTENT_GUIDE.md](LEARNING_CONTENT_GUIDE.md) - [SWAGGER_GUIDE.md](SWAGGER_GUIDE.md)