Files
sena_db_api_layer/API_STRUCTURE_GUIDE.md
Ken 6287a019e3
All checks were successful
Deploy to Production / deploy (push) Successful in 20s
update
2026-02-27 09:38:39 +07:00

17 KiB

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

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

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:

# 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

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

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:

# 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

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

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:

# 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

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)

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:

# 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

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)

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:

# 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)

POST /api/categories/:categoryId/subjects
POST /api/subjects/:subjectId/chapters
POST /api/chapters/:chapterId/lessons

Quan hệ N:N (Many-to-Many)

# 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

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

// 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

// 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

// 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)

// 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

// ✅ Đú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

// ✅ Đúng: Consistent response structure
{
  "success": true,
  "message": "Subject added to category successfully",
  "data": { ...subject }
}

// ❌ Sai: Inconsistent
{
  "subject": { ...subject },
  "msg": "OK"
}

3. Error Handling

// ✅ Đú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:

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:

{
  "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:

DELETE /api/subjects/456e7890-e89b-12d3-a456-426614174001/chapters/789e0123-e89b-12d3-a456-426614174002

Response (Success):

{
  "success": true,
  "message": "Chapter removed from subject successfully"
}

Response (Error - Chapter có lessons):

{
  "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:

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:

{
  "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:

GET /api/lessons/012e3456-e89b-12d3-a456-426614174003/stories?page=1&limit=10

Response:

{
  "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:

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:

{
  "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ũ)

// 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)

// 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