migration to typescript

This commit is contained in:
JakkrapartXD 2026-01-09 06:28:15 +00:00
parent 924000b084
commit 9fde77468a
41 changed files with 11952 additions and 10164 deletions

View file

@ -4,63 +4,214 @@ 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.
Follow these steps to create a new API endpoint for the E-Learning Platform backend using TypeScript and TSOA.
---
## Step 1: Define Route
## Step 1: Define Route with TSOA Controller
Create or update route file in `src/routes/`:
Create or update controller file in `src/controllers/`:
```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');
```typescript
// src/controllers/course.controller.ts
import { Request, Response } from 'express';
import {
Controller,
Get,
Post,
Put,
Delete,
Route,
Tags,
Security,
Body,
Path,
Query,
SuccessResponse
} from 'tsoa';
import { courseService } from '../services/course.service';
import { CreateCourseDto, UpdateCourseDto, CourseListQuery } from '../types/course.types';
// Public routes
router.get('/', courseController.list);
router.get('/:id', courseController.getById);
@Route('api/courses')
@Tags('Courses')
export class CourseController extends Controller {
/**
* Get list of courses
* @summary List all approved courses
*/
@Get('/')
@SuccessResponse(200, 'Success')
public async list(
@Query() page: number = 1,
@Query() limit: number = 20,
@Query() category?: number,
@Query() search?: string
): Promise<{
data: any[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}> {
const result = await courseService.list({
page: parseInt(page.toString()),
limit: parseInt(limit.toString()),
category: category ? parseInt(category.toString()) : undefined,
search
});
// Instructor routes
router.post(
'/',
authenticate,
authorize(['INSTRUCTOR', 'ADMIN']),
validate(courseSchema.create),
courseController.create
);
return result;
}
router.put(
'/:id',
authenticate,
authorize(['INSTRUCTOR', 'ADMIN']),
validate(courseSchema.update),
courseController.update
);
/**
* Get course by ID
* @summary Get course details
*/
@Get('{id}')
@SuccessResponse(200, 'Success')
public async getById(@Path() id: number): Promise<any> {
const course = await courseService.getById(parseInt(id.toString()));
module.exports = router;
if (!course) {
this.setStatus(404);
throw new Error('Course not found');
}
return course;
}
/**
* Create a new course
* @summary Create course (Instructor/Admin only)
*/
@Post('/')
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
@SuccessResponse(201, 'Created')
public async create(
@Body() body: CreateCourseDto,
@Request() req: any
): Promise<any> {
const course = await courseService.create(body, req.user.id);
this.setStatus(201);
return course;
}
/**
* Update course
* @summary Update course (Instructor/Admin only)
*/
@Put('{id}')
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
@SuccessResponse(200, 'Success')
public async update(
@Path() id: number,
@Body() body: UpdateCourseDto,
@Request() req: any
): Promise<any> {
// Check ownership
const course = await courseService.getById(parseInt(id.toString()));
if (!course) {
this.setStatus(404);
throw new Error('Course not found');
}
if (course.created_by !== req.user.id && req.user.role.code !== 'ADMIN') {
this.setStatus(403);
throw new Error('You do not have permission to update this course');
}
const updated = await courseService.update(parseInt(id.toString()), body);
return updated;
}
}
```
---
## Step 2: Create Validator
## Step 2: Create Type Definitions
Create types in `src/types/`:
```typescript
// src/types/course.types.ts
/**
* Multi-language text structure
*/
export interface MultiLang {
th: string;
en: string;
}
/**
* Create course DTO
*/
export interface CreateCourseDto {
/** Course title in Thai and English */
title: MultiLang;
/** Course description in Thai and English */
description: MultiLang;
/** Category ID */
category_id: number;
/** Course price */
price: number;
/** Is the course free? */
is_free?: boolean;
/** Does the course have a certificate? */
have_certificate?: boolean;
/** Course thumbnail URL */
thumbnail?: string;
}
/**
* Update course DTO
*/
export interface UpdateCourseDto {
title?: MultiLang;
description?: MultiLang;
category_id?: number;
price?: number;
is_free?: boolean;
have_certificate?: boolean;
thumbnail?: string;
}
/**
* Course list query parameters
*/
export interface CourseListQuery {
page?: number;
limit?: number;
category?: number;
search?: string;
}
```
---
## Step 3: Create Validation Schemas
Create validator in `src/validators/`:
```javascript
// src/validators/course.validator.js
const Joi = require('joi');
```typescript
// src/validators/course.validator.ts
import Joi from 'joi';
const multiLangSchema = Joi.object({
th: Joi.string().required(),
en: Joi.string().required()
});
const courseSchema = {
export const courseSchema = {
create: Joi.object({
title: multiLangSchema.required(),
description: multiLangSchema.required(),
@ -81,124 +232,6 @@ const courseSchema = {
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();
```
---
@ -207,16 +240,18 @@ module.exports = new CourseController();
Create service in `src/services/`:
```javascript
// src/services/course.service.js
const { PrismaClient } = require('@prisma/client');
```typescript
// src/services/course.service.ts
import { PrismaClient } from '@prisma/client';
import { CreateCourseDto, UpdateCourseDto, CourseListQuery } from '../types/course.types';
const prisma = new PrismaClient();
class CourseService {
async list({ page, limit, category, search }) {
const skip = (page - 1) * limit;
async list({ page, limit, category, search }: CourseListQuery) {
const skip = ((page || 1) - 1) * (limit || 20);
const where = {
const where: any = {
is_deleted: false,
status: 'APPROVED'
};
@ -236,7 +271,7 @@ class CourseService {
prisma.course.findMany({
where,
skip,
take: limit,
take: limit || 20,
include: {
category: true,
instructors: {
@ -252,15 +287,15 @@ class CourseService {
return {
data,
pagination: {
page,
limit,
page: page || 1,
limit: limit || 20,
total,
totalPages: Math.ceil(total / limit)
totalPages: Math.ceil(total / (limit || 20))
}
};
}
async getById(id) {
async getById(id: number) {
return prisma.course.findUnique({
where: { id },
include: {
@ -285,7 +320,7 @@ class CourseService {
});
}
async create(data, userId) {
async create(data: CreateCourseDto, userId: number) {
return prisma.course.create({
data: {
...data,
@ -307,7 +342,7 @@ class CourseService {
});
}
async update(id, data) {
async update(id: number, data: UpdateCourseDto) {
return prisma.course.update({
where: { id },
data,
@ -321,46 +356,100 @@ class CourseService {
}
}
module.exports = new CourseService();
export const courseService = new CourseService();
```
---
## Step 5: Register Route
## Step 5: Generate TSOA Routes and Swagger Docs
Update `src/app.js`:
// turbo
After creating the controller, generate routes and API documentation:
```bash
# Generate TSOA routes and Swagger spec
npm run tsoa:gen
# This will:
# 1. Generate routes in src/routes/tsoa-routes.ts
# 2. Generate swagger.json in public/swagger.json
# 3. Validate all TSOA decorators
```
---
## Step 6: Register Routes in App
Update `src/app.ts`:
```typescript
// src/app.ts
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import { RegisterRoutes } from './routes/tsoa-routes';
```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');
// Register TSOA routes
RegisterRoutes(app);
app.use('/api/auth', authRoutes);
app.use('/api/courses', courseRoutes);
// Swagger documentation
app.use('/api-docs', swaggerUi.serve, async (_req, res) => {
return res.send(
swaggerUi.generateHTML(await import('../public/swagger.json'))
);
});
module.exports = app;
export default app;
```
---
## Step 6: Write Tests
## Step 7: Configure TSOA
Create `tsoa.json` in project root:
```json
{
"entryFile": "src/app.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/**/*.controller.ts"],
"spec": {
"outputDirectory": "public",
"specVersion": 3,
"securityDefinitions": {
"jwt": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"routes": {
"routesDir": "src/routes",
"middleware": "express",
"authenticationModule": "./src/middleware/auth.ts"
}
}
```
---
## Step 8: Write Tests
Create test file in `tests/integration/`:
```javascript
// tests/integration/courses.test.js
const request = require('supertest');
const app = require('../../src/app');
```typescript
// tests/integration/courses.test.ts
import request from 'supertest';
import app from '../../src/app';
describe('Course API', () => {
let instructorToken;
let instructorToken: string;
beforeAll(async () => {
const res = await request(app)
@ -423,94 +512,118 @@ describe('Course API', () => {
---
## Step 7: Run Tests
## Step 9: Run Tests
// turbo
```bash
npm test -- courses.test.js
npm test -- courses.test.ts
```
---
## Step 8: Test Manually
## Step 10: View API Documentation
// turbo
```bash
# 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
}'
After generating TSOA routes, access Swagger UI:
# List courses
curl http://localhost:4000/api/courses?page=1&limit=10
```
http://localhost:4000/api-docs
```
---
## Checklist
- [ ] Route defined with proper middleware
- [ ] Validator created with Joi/Zod
- [ ] Controller created with error handling
- [ ] Controller created with TSOA decorators
- [ ] Type definitions created
- [ ] Validator created with Joi
- [ ] Service created with business logic
- [ ] Route registered in app.js
- [ ] TSOA routes generated (`npm run tsoa:gen`)
- [ ] Routes registered in app.ts
- [ ] Tests written (unit + integration)
- [ ] All tests passing
- [ ] API documentation accessible at /api-docs
- [ ] 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
1. **TSOA Decorators**: Use proper decorators (@Get, @Post, @Security, etc.)
2. **Type Safety**: Define interfaces for all DTOs
3. **Documentation**: Add JSDoc comments for Swagger descriptions
4. **Validation**: Validate input at controller level
5. **Authorization**: Use @Security decorator for protected routes
6. **Multi-Language**: Use JSON structure for user-facing text
7. **Pagination**: Always paginate list endpoints
8. **Testing**: Write tests before deploying
9. **Auto-Generate**: Always run `npm run tsoa:gen` after route changes
---
## Common Patterns
## Common TSOA Patterns
### File Upload Endpoint
```javascript
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
```typescript
import { UploadedFile } from 'express-fileupload';
router.post(
'/upload',
authenticate,
upload.single('file'),
validate(uploadSchema),
controller.upload
);
@Post('upload')
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
public async upload(
@UploadedFile() file: Express.Multer.File
): Promise<{ url: string }> {
const result = await uploadService.uploadFile(file);
return { url: result.url };
}
```
### 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' });
```typescript
@Put('{id}')
@Security('jwt')
public async update(
@Path() id: number,
@Body() body: UpdateDto,
@Request() req: any
): Promise<any> {
const resource = await service.getById(id);
if (!resource) {
this.setStatus(404);
throw new Error('Not found');
}
if (resource.user_id !== req.user.id && req.user.role.code !== 'ADMIN') {
this.setStatus(403);
throw new Error('Forbidden');
}
return await service.update(id, body);
}
```
### Soft Delete
```javascript
// In service
async delete(id) {
return prisma.course.update({
where: { id },
data: { is_deleted: true, deleted_at: new Date() }
});
```typescript
@Delete('{id}')
@Security('jwt')
public async delete(@Path() id: number): Promise<void> {
await service.softDelete(id);
this.setStatus(204);
}
```
---
## Package.json Scripts
Add these scripts to `package.json`:
```json
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "npm run tsoa:gen && tsc",
"tsoa:gen": "tsoa spec-and-routes",
"start": "node dist/server.js",
"test": "jest"
}
}
```

View file

@ -4,7 +4,7 @@ description: Complete workflow for developing E-Learning Platform backend
# E-Learning Backend Development Workflow
Complete guide for developing the E-Learning Platform backend from scratch.
Complete guide for developing the E-Learning Platform backend using TypeScript and TSOA.
---
@ -28,6 +28,10 @@ cd e-learning/Backend
# Install dependencies
npm install
# Install TypeScript and TSOA
npm install -D typescript @types/node @types/express ts-node nodemon
npm install tsoa swagger-ui-express
# Setup environment
cp .env.example .env
# Edit .env with your configuration
@ -126,72 +130,12 @@ npx prisma migrate dev --name initial_schema
- `POST /api/auth/refresh` - Refresh token
**Implementation steps:**
1. Create `src/routes/auth.routes.js`
2. Create `src/controllers/auth.controller.js`
3. Create `src/services/auth.service.js`
4. Implement JWT middleware
5. Write tests
1. Create TSOA controller with `@Route`, `@Post` decorators
2. Create TypeScript service with interfaces
3. Implement JWT middleware
4. Write tests
**Example:**
```javascript
// src/services/auth.service.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
class AuthService {
async register({ username, email, password, role_id = 3 }) {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
username,
email,
password: hashedPassword,
role_id
},
include: { role: true }
});
const token = this.generateToken(user);
return { user, token };
}
async login({ username, email, password }) {
const user = await prisma.user.findFirst({
where: {
OR: [
{ username },
{ email }
]
},
include: { role: true }
});
if (!user || !await bcrypt.compare(password, user.password)) {
throw new Error('Invalid credentials');
}
const token = this.generateToken(user);
return { user, token };
}
generateToken(user) {
return jwt.sign(
{
userId: user.id,
username: user.username,
role: user.role.code
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN }
);
}
}
module.exports = new AuthService();
```
See [Create API Endpoint Workflow](./create-api-endpoint.md) for detailed examples.
### 3.2 Course Management
@ -324,41 +268,11 @@ model Course {
### 5.2 Caching (Redis)
Implement caching for:
- Course listings
- User sessions
- Frequently accessed data
```javascript
const redis = require('redis');
const client = redis.createClient({
url: process.env.REDIS_URL
});
// Cache course list
const cacheKey = 'courses:approved';
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const courses = await prisma.course.findMany(...);
await client.setEx(cacheKey, 3600, JSON.stringify(courses));
```
Cache course listings, user sessions, and frequently accessed data using Redis with `setEx()` for TTL.
### 5.3 Rate Limiting
```javascript
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
```
Use `express-rate-limit` middleware to limit requests (e.g., 100 requests per 15 minutes).
---
@ -379,16 +293,7 @@ app.use('/api/', limiter);
### 6.2 Implement Security Middleware
```javascript
const helmet = require('helmet');
const cors = require('cors');
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN.split(','),
credentials: true
}));
```
Use `helmet()` for security headers and configure CORS with allowed origins.
---
@ -396,15 +301,25 @@ app.use(cors({
### 7.1 API Documentation
Use Swagger/OpenAPI:
TSOA automatically generates Swagger/OpenAPI documentation:
```javascript
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');
```typescript
import swaggerUi from 'swagger-ui-express';
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.use('/api-docs', swaggerUi.serve, async (_req, res) => {
return res.send(
swaggerUi.generateHTML(await import('../public/swagger.json'))
);
});
```
**Generate documentation:**
```bash
npm run tsoa:gen
```
Access at: `http://localhost:4000/api-docs`
### 7.2 Code Documentation
- JSDoc comments for all functions
@ -500,35 +415,9 @@ git push origin feature/user-authentication
## Troubleshooting
### Common Issues
**Database connection error:**
```bash
# Check PostgreSQL
docker ps | grep postgres
docker logs elearning-postgres
# Test connection
npx prisma db pull
```
**Port already in use:**
```bash
# Find process
lsof -i :4000
# Kill process
kill -9 <PID>
```
**Prisma Client error:**
```bash
# Regenerate client
npx prisma generate
# Reset and migrate
npx prisma migrate reset
```
- **Database error**: Check `docker logs elearning-postgres`, run `npx prisma db pull`
- **Port in use**: Find with `lsof -i :4000`, kill with `kill -9 <PID>`
- **Prisma error**: Run `npx prisma generate` or `npx prisma migrate reset`
---
@ -545,7 +434,9 @@ npx prisma migrate reset
```bash
# Development
npm run dev # Start dev server
npm run dev # Start dev server (ts-node)
npm run build # Build TypeScript
npm run tsoa:gen # Generate TSOA routes & Swagger
npm test # Run tests
npm run lint # Run linter
npm run format # Format code

View file

@ -4,54 +4,61 @@ description: How to handle file uploads (videos, attachments)
# File Upload Workflow
Follow these steps to implement file upload functionality for videos and attachments.
Follow these steps to implement file upload functionality for videos and attachments using TypeScript and TSOA.
---
## Prerequisites
- MinIO/S3 configured and running
- Multer installed: `npm install multer`
- AWS SDK or MinIO client installed
- Dependencies installed: `npm install multer @aws-sdk/client-s3 @aws-sdk/lib-storage`
- TypeScript configured
---
## Step 1: Configure S3/MinIO Client
Create `src/config/s3.config.js`:
Create `src/config/s3.config.ts`:
```javascript
const { S3Client } = require('@aws-sdk/client-s3');
const { Upload } = require('@aws-sdk/lib-storage');
```typescript
import { S3Client } from '@aws-sdk/client-s3';
const s3Client = new S3Client({
export const s3Client = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!
},
forcePathStyle: true // Required for MinIO
});
module.exports = { s3Client };
```
---
## Step 2: Create Upload Service
Create `src/services/upload.service.js`:
Create `src/services/upload.service.ts`:
```javascript
const { s3Client } = require('../config/s3.config');
const { Upload } = require('@aws-sdk/lib-storage');
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
```typescript
import { s3Client } from '../config/s3.config';
import { Upload } from '@aws-sdk/lib-storage';
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
export interface UploadResult {
key: string;
url: string;
fileName: string;
fileSize: number;
mimeType: string;
}
type FolderType = 'videos' | 'documents' | 'images' | 'attachments';
class UploadService {
async uploadFile(file, folder) {
async uploadFile(file: Express.Multer.File, folder: FolderType): Promise<UploadResult> {
const fileExt = path.extname(file.originalname);
const fileName = `${Date.now()}-${uuidv4()}${fileExt}`;
const key = `${folder}/${fileName}`;
@ -77,7 +84,7 @@ class UploadService {
};
}
async deleteFile(key, folder) {
async deleteFile(key: string, folder: FolderType): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: this.getBucket(folder),
Key: key
@ -86,179 +93,182 @@ class UploadService {
await s3Client.send(command);
}
getBucket(folder) {
const bucketMap = {
videos: process.env.S3_BUCKET_VIDEOS,
documents: process.env.S3_BUCKET_DOCUMENTS,
images: process.env.S3_BUCKET_IMAGES,
attachments: process.env.S3_BUCKET_ATTACHMENTS
private getBucket(folder: FolderType): string {
const bucketMap: Record<FolderType, string> = {
videos: process.env.S3_BUCKET_VIDEOS!,
documents: process.env.S3_BUCKET_DOCUMENTS!,
images: process.env.S3_BUCKET_IMAGES!,
attachments: process.env.S3_BUCKET_ATTACHMENTS!
};
return bucketMap[folder] || process.env.S3_BUCKET_COURSES;
return bucketMap[folder] || process.env.S3_BUCKET_COURSES!;
}
validateFileType(file, allowedTypes) {
validateFileType(file: Express.Multer.File, allowedTypes: string[]): boolean {
return allowedTypes.includes(file.mimetype);
}
validateFileSize(file, maxSize) {
validateFileSize(file: Express.Multer.File, maxSize: number): boolean {
return file.size <= maxSize;
}
}
module.exports = new UploadService();
export const uploadService = new UploadService();
```
---
## Step 3: Create Upload Middleware
Create `src/middleware/upload.middleware.js`:
Create `src/middleware/upload.middleware.ts`:
```javascript
const multer = require('multer');
```typescript
import multer from 'multer';
import { Request } from 'express';
// File type validators
const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
const ALLOWED_DOCUMENT_TYPES = [
export const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'];
export const ALLOWED_DOCUMENT_TYPES = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
// File size limits
const MAX_VIDEO_SIZE = 500 * 1024 * 1024; // 500 MB
const MAX_ATTACHMENT_SIZE = 100 * 1024 * 1024; // 100 MB
const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB
export const MAX_VIDEO_SIZE = 500 * 1024 * 1024; // 500 MB
export const MAX_ATTACHMENT_SIZE = 100 * 1024 * 1024; // 100 MB
export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10 MB
// Multer configuration
const storage = multer.memoryStorage();
const fileFilter = (allowedTypes) => (req, file, cb) => {
const fileFilter = (allowedTypes: string[]) => (
req: Request,
file: Express.Multer.File,
cb: multer.FileFilterCallback
) => {
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
cb(new Error('Invalid file type'));
}
};
// Upload configurations
const uploadVideo = multer({
export const uploadVideo = multer({
storage,
limits: { fileSize: MAX_VIDEO_SIZE },
fileFilter: fileFilter(ALLOWED_VIDEO_TYPES)
}).single('video');
const uploadAttachment = multer({
export const uploadAttachment = multer({
storage,
limits: { fileSize: MAX_ATTACHMENT_SIZE },
fileFilter: fileFilter([...ALLOWED_DOCUMENT_TYPES, ...ALLOWED_IMAGE_TYPES])
}).single('file');
const uploadAttachments = multer({
export const uploadAttachments = multer({
storage,
limits: { fileSize: MAX_ATTACHMENT_SIZE },
fileFilter: fileFilter([...ALLOWED_DOCUMENT_TYPES, ...ALLOWED_IMAGE_TYPES])
}).array('attachments', 10); // Max 10 files
const uploadImage = multer({
export const uploadImage = multer({
storage,
limits: { fileSize: MAX_IMAGE_SIZE },
fileFilter: fileFilter(ALLOWED_IMAGE_TYPES)
}).single('image');
module.exports = {
uploadVideo,
uploadAttachment,
uploadAttachments,
uploadImage,
ALLOWED_VIDEO_TYPES,
ALLOWED_DOCUMENT_TYPES,
ALLOWED_IMAGE_TYPES
};
```
---
## Step 4: Create Upload Controller
## Step 4: Create Upload Controller with TSOA
Create `src/controllers/upload.controller.js`:
Create `src/controllers/upload.controller.ts`:
```javascript
const uploadService = require('../services/upload.service');
```typescript
import { Request } from 'express';
import { Controller, Post, Route, Tags, Security, UploadedFile, SuccessResponse } from 'tsoa';
import { uploadService } from '../services/upload.service';
class UploadController {
async uploadVideo(req, res) {
try {
if (!req.file) {
return res.status(400).json({
error: {
code: 'NO_FILE',
message: 'No video file provided'
}
});
}
const result = await uploadService.uploadFile(
req.file,
'videos'
);
return res.status(200).json({
video_url: result.url,
file_size: result.fileSize,
duration: null // Will be processed later
});
} catch (error) {
console.error('Upload video error:', error);
return res.status(500).json({
error: {
code: 'UPLOAD_FAILED',
message: 'Failed to upload video'
}
});
}
}
async uploadAttachment(req, res) {
try {
if (!req.file) {
return res.status(400).json({
error: {
code: 'NO_FILE',
message: 'No file provided'
}
});
}
const result = await uploadService.uploadFile(
req.file,
'attachments'
);
return res.status(200).json({
file_name: result.fileName,
file_path: result.key,
file_size: result.fileSize,
mime_type: result.mimeType,
download_url: result.url
});
} catch (error) {
console.error('Upload attachment error:', error);
return res.status(500).json({
error: {
code: 'UPLOAD_FAILED',
message: 'Failed to upload file'
}
});
}
}
interface UploadResponse {
video_url?: string;
file_size: number;
duration?: number | null;
file_name?: string;
file_path?: string;
mime_type?: string;
download_url?: string;
}
module.exports = new UploadController();
@Route('api/upload')
@Tags('File Upload')
export class UploadController extends Controller {
/**
* Upload video file
* @summary Upload video for lesson (Instructor/Admin only)
*/
@Post('video')
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
@SuccessResponse(200, 'Success')
public async uploadVideo(
@UploadedFile() file: Express.Multer.File
): Promise<UploadResponse> {
if (!file) {
this.setStatus(400);
throw new Error('No video file provided');
}
const result = await uploadService.uploadFile(file, 'videos');
return {
video_url: result.url,
file_size: result.fileSize,
duration: null // Will be processed later
};
}
/**
* Upload attachment file
* @summary Upload attachment for lesson (Instructor/Admin only)
*/
@Post('attachment')
@Security('jwt', ['INSTRUCTOR', 'ADMIN'])
@SuccessResponse(200, 'Success')
public async uploadAttachment(
@UploadedFile() file: Express.Multer.File
): Promise<UploadResponse> {
if (!file) {
this.setStatus(400);
throw new Error('No file provided');
}
const result = await uploadService.uploadFile(file, 'attachments');
return {
file_name: result.fileName,
file_path: result.key,
file_size: result.fileSize,
mime_type: result.mimeType,
download_url: result.url
};
}
}
```
---
## Step 5: Generate TSOA Routes
// turbo
After creating the upload controller, generate routes and API documentation:
```bash
npm run tsoa:gen
```
---

View file

@ -4,7 +4,7 @@ description: How to setup the backend development environment
# Setup Development Environment
Follow these steps to setup the E-Learning Platform backend on your local machine.
Follow these steps to setup the E-Learning Platform backend with TypeScript and TSOA on your local machine.
---
@ -32,9 +32,77 @@ cd e-learning/Backend
npm install
```
// turbo
Install TypeScript and TSOA:
```bash
npm install -D typescript @types/node @types/express ts-node nodemon
npm install tsoa swagger-ui-express
```
---
## Step 3: Setup Environment Variables
## Step 3: Initialize TypeScript
// turbo
Create `tsconfig.json`:
```bash
npx tsc --init
```
Update `tsconfig.json`:
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
---
## Step 4: Configure TSOA
Create `tsoa.json`:
```json
{
"entryFile": "src/app.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/controllers/**/*.controller.ts"],
"spec": {
"outputDirectory": "public",
"specVersion": 3,
"securityDefinitions": {
"jwt": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"routes": {
"routesDir": "src/routes",
"middleware": "express",
"authenticationModule": "./src/middleware/auth.ts"
}
}
```
---
## Step 5: Setup Environment Variables
Copy example env file:
```bash
@ -92,7 +160,7 @@ This starts:
---
## Step 5: Run Database Migrations
## Step 6: Run Database Migrations
// turbo
```bash
@ -101,7 +169,7 @@ npx prisma migrate dev
---
## Step 6: Seed Database
## Step 7: Seed Database
// turbo
```bash
@ -116,7 +184,20 @@ This creates:
---
## Step 7: Start Development Server
## Step 8: Generate TSOA Routes
// turbo
```bash
npm run tsoa:gen
```
This generates:
- Routes in `src/routes/tsoa-routes.ts`
- Swagger spec in `public/swagger.json`
---
## Step 9: Start Development Server
// turbo
```bash
@ -127,7 +208,7 @@ Server will start at http://localhost:4000
---
## Step 8: Verify Setup
## Step 10: Verify Setup
// turbo
Test health endpoint:
@ -150,6 +231,7 @@ curl -X POST http://localhost:4000/api/auth/login \
| Service | URL | Credentials |
|---------|-----|-------------|
| **Backend API** | http://localhost:4000 | - |
| **API Docs (Swagger)** | http://localhost:4000/api-docs | - |
| **MinIO Console** | http://localhost:9001 | admin / 12345678 |
| **Mailhog UI** | http://localhost:8025 | - |
| **Adminer** | http://localhost:8080 | postgres / 12345678 |
@ -159,9 +241,15 @@ curl -X POST http://localhost:4000/api/auth/login \
## Development Commands
```bash
# Start dev server
# Start dev server (TypeScript)
npm run dev
# Build TypeScript
npm run build
# Generate TSOA routes and Swagger
npm run tsoa:gen
# Run tests
npm test

View file

@ -4,7 +4,7 @@ description: How to test backend APIs and services
# Testing Workflow
Follow these steps to write and run tests for the E-Learning Platform backend.
Follow these steps to write and run tests for the E-Learning Platform backend using TypeScript and Jest.
---
@ -22,10 +22,12 @@ tests/
## Step 1: Setup Test Environment
Create `tests/setup.js`:
Create `tests/setup.ts`:
```typescript
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';
```javascript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
beforeAll(async () => {
@ -44,8 +46,6 @@ afterAll(async () => {
async function createTestUsers() {
// Create admin, instructor, student
const bcrypt = require('bcrypt');
await prisma.user.createMany({
data: [
{
@ -75,11 +75,12 @@ async function createTestUsers() {
## Step 2: Write Unit Tests
Create `tests/unit/course.service.test.js`:
Create `tests/unit/course.service.test.ts`:
```typescript
import { courseService } from '../../src/services/course.service';
import { PrismaClient } from '@prisma/client';
```javascript
const courseService = require('../../src/services/course.service');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
describe('Course Service', () => {
@ -93,7 +94,7 @@ describe('Course Service', () => {
is_free: false
};
const course = await courseService.createCourse(courseData, 1);
const course = await courseService.create(courseData, 1);
expect(course).toHaveProperty('id');
expect(course.status).toBe('DRAFT');
@ -108,7 +109,7 @@ describe('Course Service', () => {
};
await expect(
courseService.createCourse(courseData, 1)
courseService.create(courseData, 1)
).rejects.toThrow();
});
});
@ -134,14 +135,16 @@ describe('Course Service', () => {
## Step 3: Write Integration Tests
Create `tests/integration/courses.test.js`:
Create `tests/integration/courses.test.ts`:
```javascript
const request = require('supertest');
const app = require('../../src/app');
```typescript
import request from 'supertest';
import app from '../../src/app';
describe('Course API', () => {
let adminToken, instructorToken, studentToken;
let adminToken: string;
let instructorToken: string;
let studentToken: string;
beforeAll(async () => {
// Login as different users
@ -217,7 +220,7 @@ describe('Course API', () => {
.query({ category: 1 });
expect(response.status).toBe(200);
response.body.data.forEach(course => {
response.body.data.forEach((course: any) => {
expect(course.category_id).toBe(1);
});
});
@ -229,15 +232,15 @@ describe('Course API', () => {
## Step 4: Test File Uploads
Create `tests/integration/file-upload.test.js`:
Create `tests/integration/file-upload.test.ts`:
```javascript
const request = require('supertest');
const app = require('../../src/app');
const path = require('path');
```typescript
import request from 'supertest';
import app from '../../src/app';
import path from 'path';
describe('File Upload API', () => {
let instructorToken;
let instructorToken: string;
beforeAll(async () => {
const res = await request(app)
@ -330,10 +333,12 @@ Update `package.json`:
"test:coverage": "jest --coverage"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"],
"setupFilesAfterEnv": ["<rootDir>/tests/setup.js"],
"testMatch": ["**/tests/**/*.test.js"]
"setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
"testMatch": ["**/tests/**/*.test.ts"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}
}
```