12 KiB
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
- Separation of Concerns: Routes → Controllers → Services → Database
- Validation: Always validate input at route level
- Authorization: Check permissions in controller
- Error Handling: Use try-catch and return proper error codes
- Multi-Language: Use JSON structure for user-facing text
- Pagination: Always paginate list endpoints
- 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() }
});
}