--- 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/`: ```javascript // 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/`: ```javascript // 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/`: ```javascript // 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/`: ```javascript // 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`: ```javascript 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/`: ```javascript // 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 ```bash npm test -- courses.test.js ``` --- ## Step 8: Test Manually // turbo ```bash # Create course curl -X POST http://localhost:4000/api/courses \ -H "Authorization: Bearer " \ -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 ```javascript const multer = require('multer'); const upload = multer({ dest: 'uploads/' }); router.post( '/upload', authenticate, upload.single('file'), validate(uploadSchema), controller.upload ); ``` ### Ownership Check ```javascript // 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 ```javascript // In service async delete(id) { return prisma.course.update({ where: { id }, data: { is_deleted: true, deleted_at: new Date() } }); } ```