elearning/Backend/.agent/workflows/create-api-endpoint.md
2026-01-08 06:51:33 +00:00

12 KiB

description
How to create a new API endpoint

Create API Endpoint Workflow

Follow these steps to create a new API endpoint for the E-Learning Platform backend.


Step 1: Define Route

Create or update route file in src/routes/:

// src/routes/courses.routes.js
const express = require('express');
const router = express.Router();
const courseController = require('../controllers/course.controller');
const { authenticate, authorize } = require('../middleware/auth');
const { validate } = require('../middleware/validation');
const { courseSchema } = require('../validators/course.validator');

// Public routes
router.get('/', courseController.list);
router.get('/:id', courseController.getById);

// Instructor routes
router.post(
  '/',
  authenticate,
  authorize(['INSTRUCTOR', 'ADMIN']),
  validate(courseSchema.create),
  courseController.create
);

router.put(
  '/:id',
  authenticate,
  authorize(['INSTRUCTOR', 'ADMIN']),
  validate(courseSchema.update),
  courseController.update
);

module.exports = router;

Step 2: Create Validator

Create validator in src/validators/:

// src/validators/course.validator.js
const Joi = require('joi');

const multiLangSchema = Joi.object({
  th: Joi.string().required(),
  en: Joi.string().required()
});

const courseSchema = {
  create: Joi.object({
    title: multiLangSchema.required(),
    description: multiLangSchema.required(),
    category_id: Joi.number().integer().positive().required(),
    price: Joi.number().min(0).required(),
    is_free: Joi.boolean().default(false),
    have_certificate: Joi.boolean().default(false),
    thumbnail: Joi.string().uri().optional()
  }),

  update: Joi.object({
    title: multiLangSchema.optional(),
    description: multiLangSchema.optional(),
    category_id: Joi.number().integer().positive().optional(),
    price: Joi.number().min(0).optional(),
    is_free: Joi.boolean().optional(),
    have_certificate: Joi.boolean().optional(),
    thumbnail: Joi.string().uri().optional()
  })
};

module.exports = { courseSchema };

Step 3: Create Controller

Create controller in src/controllers/:

// src/controllers/course.controller.js
const courseService = require('../services/course.service');

class CourseController {
  async list(req, res) {
    try {
      const { page = 1, limit = 20, category, search } = req.query;
      
      const result = await courseService.list({
        page: parseInt(page),
        limit: parseInt(limit),
        category: category ? parseInt(category) : undefined,
        search
      });

      return res.status(200).json(result);
    } catch (error) {
      console.error('List courses error:', error);
      return res.status(500).json({
        error: {
          code: 'INTERNAL_ERROR',
          message: 'Failed to fetch courses'
        }
      });
    }
  }

  async getById(req, res) {
    try {
      const { id } = req.params;
      const course = await courseService.getById(parseInt(id));

      if (!course) {
        return res.status(404).json({
          error: {
            code: 'COURSE_NOT_FOUND',
            message: 'Course not found'
          }
        });
      }

      return res.status(200).json(course);
    } catch (error) {
      console.error('Get course error:', error);
      return res.status(500).json({
        error: {
          code: 'INTERNAL_ERROR',
          message: 'Failed to fetch course'
        }
      });
    }
  }

  async create(req, res) {
    try {
      const course = await courseService.create(req.body, req.user.id);
      return res.status(201).json(course);
    } catch (error) {
      console.error('Create course error:', error);
      return res.status(500).json({
        error: {
          code: 'INTERNAL_ERROR',
          message: 'Failed to create course'
        }
      });
    }
  }

  async update(req, res) {
    try {
      const { id } = req.params;
      
      // Check ownership
      const course = await courseService.getById(parseInt(id));
      if (!course) {
        return res.status(404).json({
          error: {
            code: 'COURSE_NOT_FOUND',
            message: 'Course not found'
          }
        });
      }

      if (course.created_by !== req.user.id && req.user.role.code !== 'ADMIN') {
        return res.status(403).json({
          error: {
            code: 'FORBIDDEN',
            message: 'You do not have permission to update this course'
          }
        });
      }

      const updated = await courseService.update(parseInt(id), req.body);
      return res.status(200).json(updated);
    } catch (error) {
      console.error('Update course error:', error);
      return res.status(500).json({
        error: {
          code: 'INTERNAL_ERROR',
          message: 'Failed to update course'
        }
      });
    }
  }
}

module.exports = new CourseController();

Step 4: Create Service

Create service in src/services/:

// src/services/course.service.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

class CourseService {
  async list({ page, limit, category, search }) {
    const skip = (page - 1) * limit;

    const where = {
      is_deleted: false,
      status: 'APPROVED'
    };

    if (category) {
      where.category_id = category;
    }

    if (search) {
      where.OR = [
        { title: { path: ['th'], string_contains: search } },
        { title: { path: ['en'], string_contains: search } }
      ];
    }

    const [data, total] = await Promise.all([
      prisma.course.findMany({
        where,
        skip,
        take: limit,
        include: {
          category: true,
          instructors: {
            where: { is_primary: true },
            include: { user: { include: { profile: true } } }
          }
        },
        orderBy: { created_at: 'desc' }
      }),
      prisma.course.count({ where })
    ]);

    return {
      data,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit)
      }
    };
  }

  async getById(id) {
    return prisma.course.findUnique({
      where: { id },
      include: {
        category: true,
        instructors: {
          include: { user: { include: { profile: true } } }
        },
        chapters: {
          include: {
            lessons: {
              select: {
                id: true,
                title: true,
                type: true,
                sort_order: true
              }
            }
          },
          orderBy: { sort_order: 'asc' }
        }
      }
    });
  }

  async create(data, userId) {
    return prisma.course.create({
      data: {
        ...data,
        status: 'DRAFT',
        created_by: userId,
        instructors: {
          create: {
            user_id: userId,
            is_primary: true
          }
        }
      },
      include: {
        category: true,
        instructors: {
          include: { user: { include: { profile: true } } }
        }
      }
    });
  }

  async update(id, data) {
    return prisma.course.update({
      where: { id },
      data,
      include: {
        category: true,
        instructors: {
          include: { user: { include: { profile: true } } }
        }
      }
    });
  }
}

module.exports = new CourseService();

Step 5: Register Route

Update src/app.js:

const express = require('express');
const app = express();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
const authRoutes = require('./routes/auth.routes');
const courseRoutes = require('./routes/courses.routes');

app.use('/api/auth', authRoutes);
app.use('/api/courses', courseRoutes);

module.exports = app;

Step 6: Write Tests

Create test file in tests/integration/:

// tests/integration/courses.test.js
const request = require('supertest');
const app = require('../../src/app');

describe('Course API', () => {
  let instructorToken;

  beforeAll(async () => {
    const res = await request(app)
      .post('/api/auth/login')
      .send({ username: 'instructor1', password: 'password123' });
    instructorToken = res.body.token;
  });

  describe('POST /api/courses', () => {
    it('should create course with valid data', async () => {
      const courseData = {
        title: { th: 'คอร์สทดสอบ', en: 'Test Course' },
        description: { th: 'รายละเอียด', en: 'Description' },
        category_id: 1,
        price: 990,
        is_free: false
      };

      const response = await request(app)
        .post('/api/courses')
        .set('Authorization', `Bearer ${instructorToken}`)
        .send(courseData);

      expect(response.status).toBe(201);
      expect(response.body).toHaveProperty('id');
      expect(response.body.status).toBe('DRAFT');
    });

    it('should return 400 with invalid data', async () => {
      const response = await request(app)
        .post('/api/courses')
        .set('Authorization', `Bearer ${instructorToken}`)
        .send({ title: 'Invalid' });

      expect(response.status).toBe(400);
    });

    it('should return 401 without auth', async () => {
      const response = await request(app)
        .post('/api/courses')
        .send({});

      expect(response.status).toBe(401);
    });
  });

  describe('GET /api/courses', () => {
    it('should list courses', async () => {
      const response = await request(app)
        .get('/api/courses')
        .query({ page: 1, limit: 10 });

      expect(response.status).toBe(200);
      expect(response.body).toHaveProperty('data');
      expect(response.body).toHaveProperty('pagination');
    });
  });
});

Step 7: Run Tests

// turbo

npm test -- courses.test.js

Step 8: Test Manually

// turbo

# Create course
curl -X POST http://localhost:4000/api/courses \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": {"th": "ทดสอบ", "en": "Test"},
    "description": {"th": "รายละเอียด", "en": "Description"},
    "category_id": 1,
    "price": 990
  }'

# List courses
curl http://localhost:4000/api/courses?page=1&limit=10

Checklist

  • Route defined with proper middleware
  • Validator created with Joi/Zod
  • Controller created with error handling
  • Service created with business logic
  • Route registered in app.js
  • Tests written (unit + integration)
  • All tests passing
  • Manual testing done
  • Documentation updated

Best Practices

  1. Separation of Concerns: Routes → Controllers → Services → Database
  2. Validation: Always validate input at route level
  3. Authorization: Check permissions in controller
  4. Error Handling: Use try-catch and return proper error codes
  5. Multi-Language: Use JSON structure for user-facing text
  6. Pagination: Always paginate list endpoints
  7. Testing: Write tests before deploying

Common Patterns

File Upload Endpoint

const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

router.post(
  '/upload',
  authenticate,
  upload.single('file'),
  validate(uploadSchema),
  controller.upload
);

Ownership Check

// In controller
const resource = await service.getById(id);
if (resource.user_id !== req.user.id && req.user.role.code !== 'ADMIN') {
  return res.status(403).json({ error: 'Forbidden' });
}

Soft Delete

// In service
async delete(id) {
  return prisma.course.update({
    where: { id },
    data: { is_deleted: true, deleted_at: new Date() }
  });
}